Commit 0d035441 authored by Valery Sizov's avatar Valery Sizov

Merge branch 'ce-upstream' into 'master'

Sync CE master to EE master



See merge request !346
parents 90cb7c64 ca2abf8c
......@@ -728,7 +728,7 @@ Metrics/ParameterLists:
# A complexity metric geared towards measuring complexity for a human reader.
Metrics/PerceivedComplexity:
Enabled: true
Max: 17
Max: 18
#################### Lint ################################
......
Please view this file on the master branch, on stable branches it's out of date.
v 8.7.0 (unreleased)
- The Projects::HousekeepingService class has extra instrumentation (Yorick Peterse)
- Fix revoking of authorized OAuth applications (Connor Shea)
- All service classes (those residing in app/services) are now instrumented (Yorick Peterse)
- Developers can now add custom tags to transactions (Yorick Peterse)
- Loading of an issue's referenced merge requests and related branches is now done asynchronously (Yorick Peterse)
- Method instrumentation now uses Module#prepend instead of aliasing methods
- Repository.clean_old_archives is now instrumented
- Add support for environment variables on a job level in CI configuration file
- SQL query counts are now tracked per transaction
- The Projects::HousekeepingService class has extra instrumentation
- All service classes (those residing in app/services) are now instrumented
- Developers can now add custom tags to transactions
- Loading of an issue's referenced merge requests and related branches is now done asynchronously
- Enable gzip for assets, makes the page size significantly smaller. !3544 / !3632 (Connor Shea)
- Project switcher uses new dropdown styling
- Load award emoji images separately unless opening the full picker. Saves several hundred KBs of data for most pages. (Connor Shea)
- Do not include award_emojis in issue and merge_request comment_count !3610 (Lucas Charles)
- All images in discussions and wikis now link to their source files !3464 (Connor Shea).
......@@ -14,7 +18,7 @@ v 8.7.0 (unreleased)
- Add setting for customizing the list of trusted proxies !3524
- Allow projects to be transfered to a lower visibility level group
- Fix `signed_in_ip` being set to 127.0.0.1 when using a reverse proxy !3524
- Improved Markdown rendering performance !3389 (Yorick Peterse)
- Improved Markdown rendering performance !3389
- Don't attempt to look up an avatar in repo if repo directory does not exist (Stan Hu)
- API: Ability to subscribe and unsubscribe from issues and merge requests (Robert Schilling)
- Expose project badges in project settings
......@@ -42,6 +46,7 @@ v 8.7.0 (unreleased)
- Add default scope to projects to exclude projects pending deletion
- Allow to close merge requests which source projects(forks) are deleted.
- Ensure empty recipients are rejected in BuildsEmailService
- Use rugged to change HEAD in Project#change_head (P.S.V.R)
- API: Ability to filter milestones by state `active` and `closed` (Robert Schilling)
- API: Fix milestone filtering by `iid` (Robert Schilling)
- API: Delete notes of issues, snippets, and merge requests (Robert Schilling)
......@@ -50,10 +55,12 @@ v 8.7.0 (unreleased)
- Fix high CPU usage when PostReceive receives refs/merge-requests/<id>
- Hide `Create a group` help block when creating a new project in a group
- Implement 'TODOs View' as an option for dashboard preferences !3379 (Elias W.)
- Allow issues and merge requests to be assigned to the author !2765
- Gracefully handle notes on deleted commits in merge requests (Stan Hu)
- Decouple membership and notifications
- Fix creation of merge requests for orphaned branches (Stan Hu)
- API: Ability to retrieve a single tag (Robert Schilling)
- While signing up, don't persist the user password across form redisplays
- 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)
- Fix admin/projects when using visibility levels on search (PotHix)
......@@ -69,13 +76,23 @@ v 8.7.0 (unreleased)
- Fix repository cache invalidation issue when project is recreated with an empty repo (Stan Hu)
- Fix: Allow empty recipients list for builds emails service when pushed is added (Frank Groeneveld)
- Improved markdown forms
- Delete tags using Rugged for performance reasons (Robert Schilling)
- Diffs load at the correct point when linking from from number
- Selected diff rows highlight
- Fix emoji catgories in the emoji picker
- Fix emoji categories in the emoji picker
- Add encrypted credentials for imported projects and migrate old ones
- Author and participants are displayed first on users autocompletion
- Show number sign on external issue reference text (Florent Baldino)
- Updated print style for issues
v 8.6.6
- Expire the exists cache before deletion to ensure project dir actually exists (Stan Hu). !3413
- Fix error on language detection when repository has no HEAD (e.g., master branch) (Jeroen Bobbeldijk). !3654
- Fix revoking of authorized OAuth applications (Connor Shea). !3690
- Fix error on language detection when repository has no HEAD (e.g., master branch). !3654 (Jeroen Bobbeldijk)
- Project switcher uses new dropdown styling
- Issuable header is consistent between issues and merge requests
- Improved spacing in issuable header on mobile
v 8.6.5
- Fix importing from GitHub Enterprise. !3529
......@@ -293,7 +310,7 @@ v 8.5.1
v 8.5.0
- Fix duplicate "me" in tooltip of the "thumbsup" awards Emoji (Stan Hu)
- Cache various Repository methods to improve performance (Yorick Peterse)
- Cache various Repository methods to improve performance
- Fix duplicated branch creation/deletion Webhooks/service notifications when using Web UI (Stan Hu)
- Ensure rake tasks that don't need a DB connection can be run without one
- Update New Relic gem to 3.14.1.311 (Stan Hu)
......
......@@ -2,6 +2,8 @@
window.GitLab ?= {}
GitLab.GfmAutoComplete =
dataLoading: false
dataSource: ''
# Emoji
......@@ -17,17 +19,41 @@ GitLab.GfmAutoComplete =
template: '<li><small>${id}</small> ${title}</li>'
# Add GFM auto-completion to all input fields, that accept GFM input.
setup: ->
input = $('.js-gfm-input')
setup: (wrap) ->
@input = $('.js-gfm-input')
# destroy previous instances
@destroyAtWho()
# set up instances
@setupAtWho()
if @dataSource
if !@dataLoading
@dataLoading = true
# We should wait until initializations are done
# and only trigger the last .setup since
# The previous .dataSource belongs to the previous issuable
# and the last one will have the **proper** .dataSource property
# TODO: Make this a singleton and turn off events when moving to another page
setTimeout( =>
fetch = @fetchData(@dataSource)
fetch.done (data) =>
@dataLoading = false
@loadData(data)
, 1000)
setupAtWho: ->
# Emoji
input.atwho
@input.atwho
at: ':'
displayTpl: @Emoji.template
insertTpl: ':${name}:'
# Team Members
input.atwho
@input.atwho
at: '@'
displayTpl: @Members.template
insertTpl: '${atwho-at}${username}'
......@@ -42,7 +68,7 @@ GitLab.GfmAutoComplete =
title: sanitize(title)
search: sanitize("#{m.username} #{m.name}")
input.atwho
@input.atwho
at: '#'
alias: 'issues'
searchKey: 'search'
......@@ -55,7 +81,7 @@ GitLab.GfmAutoComplete =
title: sanitize(i.title)
search: "#{i.iid} #{i.title}"
input.atwho
@input.atwho
at: '!'
alias: 'mergerequests'
searchKey: 'search'
......@@ -68,13 +94,18 @@ GitLab.GfmAutoComplete =
title: sanitize(m.title)
search: "#{m.iid} #{m.title}"
if @dataSource
$.getJSON(@dataSource).done (data) ->
# load members
input.atwho 'load', '@', data.members
# load issues
input.atwho 'load', 'issues', data.issues
# load merge requests
input.atwho 'load', 'mergerequests', data.mergerequests
# load emojis
input.atwho 'load', ':', data.emojis
destroyAtWho: ->
@input.atwho('destroy')
fetchData: (dataSource) ->
$.getJSON(dataSource)
loadData: (data) ->
# load members
@input.atwho 'load', '@', data.members
# load issues
@input.atwho 'load', 'issues', data.issues
# load merge requests
@input.atwho 'load', 'mergerequests', data.mergerequests
# load emojis
@input.atwho 'load', ':', data.emojis
......@@ -4,18 +4,33 @@ class @ImporterStatus
this.setAutoUpdate()
initStatusPage: ->
$(".js-add-to-import").click (event) =>
new_namespace = null
tr = $(event.currentTarget).closest("tr")
id = tr.attr("id").replace("repo_", "")
if tr.find(".import-target input").length > 0
new_namespace = tr.find(".import-target input").prop("value")
tr.find(".import-target").empty().append(new_namespace + "/" + tr.find(".import-target").data("project_name"))
$.post @import_url, {repo_id: id, new_namespace: new_namespace}, dataType: 'script'
$(".js-import-all").click (event) =>
$(".js-add-to-import").each ->
$(this).click()
$('.js-add-to-import')
.off 'click'
.on 'click', (e) =>
new_namespace = null
$btn = $(e.currentTarget)
$tr = $btn.closest('tr')
id = $tr.attr('id').replace('repo_', '')
if $tr.find('.import-target input').length > 0
new_namespace = $tr.find('.import-target input').prop('value')
$tr.find('.import-target').empty().append("#{new_namespace} / #{$tr.find('.import-target').data('project_name')}")
$btn
.disable()
.addClass 'is-loading'
$.post @import_url, {repo_id: id, new_namespace: new_namespace}, dataType: 'script'
$('.js-import-all')
.off 'click'
.on 'click', (e) ->
$btn = $(@)
$btn
.disable()
.addClass 'is-loading'
$('.js-add-to-import').each ->
$(this).trigger('click')
setAutoUpdate: ->
setInterval (=>
......
......@@ -75,6 +75,9 @@ class @Notes
# when issue status changes, we need to refresh data
$(document).on "issuable:change", @refresh
# when a key is clicked on the notes
$(document).on "keydown", ".js-note-text", @keydownNoteText
cleanBinding: ->
$(document).off "ajax:success", ".js-main-target-form"
$(document).off "ajax:success", ".js-discussion-note-form"
......@@ -92,10 +95,19 @@ class @Notes
$(document).off "click", ".js-note-target-reopen"
$(document).off "click", ".js-note-target-close"
$(document).off "click", ".js-note-discard"
$(document).off "keydown", ".js-note-text"
$('.note .js-task-list-container').taskList('disable')
$(document).off 'tasklist:changed', '.note .js-task-list-container'
keydownNoteText: (e) ->
$this = $(this)
if $this.val() is '' and e.which is 38 #aka the up key
myLastNote = $("li.note[data-author-id='#{gon.current_user_id}'][data-editable]:last")
if myLastNote.length
myLastNoteEditBtn = myLastNote.find('.js-note-edit')
myLastNoteEditBtn.trigger('click', [true, myLastNote])
initRefresh: ->
clearInterval(Notes.interval)
Notes.interval = setInterval =>
......@@ -343,7 +355,7 @@ class @Notes
Adds a hidden div with the original content of the note to fill the edit note form with
if the user cancels
###
showEditForm: (e) ->
showEditForm: (e, scrollTo, myLastNote) ->
e.preventDefault()
note = $(this).closest(".note")
note.addClass "is-editting"
......@@ -354,9 +366,27 @@ class @Notes
# Show the attachment delete link
note.find(".js-note-attachment-delete").show()
new GLForm form
done = ($noteText) ->
# Neat little trick to put the cursor at the end
noteTextVal = $noteText.val()
$noteText.val('').val(noteTextVal);
form.find(".js-note-text").focus()
new GLForm form
if scrollTo? and myLastNote?
# scroll to the bottom
# so the open of the last element doesn't make a jump
$('html, body').scrollTop($(document).height());
$('html, body').animate({
scrollTop: myLastNote.offset().top - 150
}, 500, ->
$noteText = form.find(".js-note-text")
$noteText.focus()
done($noteText)
);
else
$noteText = form.find('.js-note-text')
$noteText.focus()
done($noteText)
###
Called in response to clicking the edit note link
......
......@@ -12,6 +12,7 @@ class @UsersSelect
showNullUser = $dropdown.data('null-user')
showAnyUser = $dropdown.data('any-user')
firstUser = $dropdown.data('first-user')
@authorId = $dropdown.data('author-id')
selectedId = $dropdown.data('selected')
defaultLabel = $dropdown.data('default-label')
issueURL = $dropdown.data('issueUpdate')
......@@ -209,6 +210,7 @@ class @UsersSelect
@groupId = $(select).data('group-id')
@showCurrentUser = $(select).data('current-user')
@pushCodeToProtectedBranches = $(select).data('push-code-to-protected-branches')
@authorId = $(select).data('author-id')
showNullUser = $(select).data('null-user')
showAnyUser = $(select).data('any-user')
showEmailUser = $(select).data('email-user')
......@@ -316,6 +318,7 @@ class @UsersSelect
skip_ldap: @skipLdap
current_user: @showCurrentUser
push_code_to_protected_branches: @pushCodeToProtectedBranches
author_id: @authorId
dataType: "json"
).done (users) ->
callback(users)
......
......@@ -5,7 +5,7 @@
*/
.status-box {
/* Extra small devices (phones, less than 768px) */
/* No media query since this is the default in Bootstrap */
padding: 5px 11px;
......
......@@ -70,13 +70,6 @@
display: none;
}
.issue-details {
.creator,
.page-title .btn-close {
display: none;
}
}
%ul.notes .note-role, .note-actions {
display: none;
}
......
......@@ -39,8 +39,7 @@
.diff-file {
border: 1px solid $border-color;
border-bottom: none;
margin-left: 0;
margin-right: 0;
margin: 0;
}
}
......
......@@ -75,6 +75,11 @@ li.commit {
}
}
.item-title {
display: inline-block;
max-width: 70%;
}
.commit-row-description {
font-size: 14px;
border-left: 1px solid #eee;
......
.detail-page-header {
padding: 11px 0;
padding: $gl-padding-top 0;
border-bottom: 1px solid $border-color;
color: #5c5d5e;
font-size: 16px;
......@@ -16,11 +16,6 @@
.issue_created_ago, .author_link {
white-space: nowrap;
}
.issue-meta {
display: inline-block;
line-height: 20px;
}
}
.detail-page-description {
......
......@@ -16,3 +16,24 @@ i.icon-gitorious-big {
width: 18px;
height: 18px;
}
.import-jobs-from-col,
.import-jobs-to-col {
width: 40%;
}
.import-jobs-status-col {
width: 20%;
}
.btn-import {
.loading-icon {
display: none;
}
&.is-loading {
.loading-icon {
display: inline-block;
}
}
}
......@@ -273,10 +273,6 @@
}
}
.btn-default.gutter-toggle {
margin-top: 4px;
}
.detail-page-description {
small {
color: $gray-darkest;
......@@ -322,3 +318,50 @@
padding-top: 7px;
}
}
.issuable-status-box {
float: none;
display: inline-block;
margin-top: 0;
@media (max-width: $screen-xs-max) {
position: absolute;
top: 0;
left: 0;
}
}
.issuable-header {
position: relative;
padding-left: 45px;
padding-right: 45px;
line-height: 35px;
@media (min-width: $screen-sm-min) {
float: left;
padding-left: 0;
padding-right: 0;
}
}
.issuable-actions {
padding-top: 10px;
@media (min-width: $screen-sm-min) {
float: right;
padding-top: 0;
}
}
.issuable-gutter-toggle {
@media (max-width: $screen-sm-max) {
position: absolute;
top: 0;
right: 0;
}
}
.issuable-meta {
display: inline-block;
line-height: 18px;
}
......@@ -86,41 +86,9 @@ form.edit-issue {
@media (max-width: $screen-xs-max) {
.issue-btn-group {
width: 100%;
margin-top: 5px;
.btn-group {
width: 100%;
ul {
width: 100%;
text-align: center;
}
}
.btn {
width: 100%;
&:first-child:not(:last-child) {
}
&:not(:first-child):not(:last-child) {
margin-top: 10px;
}
&:last-child:not(:first-child) {
margin-top: 10px;
}
}
}
.issue {
&:hover .issue-actions {
display: none !important;
}
.issue-updated-at {
display: none;
}
}
}
......@@ -133,11 +101,3 @@ form.edit-issue {
color: $gl-text-color;
margin-left: 52px;
}
.editor-details {
display: block;
@media (min-width: $screen-sm-min) {
display: inline-block;
}
}
......@@ -198,6 +198,12 @@ ul.notes {
color: $notes-light-color;
}
.discussion-headline-light {
a {
color: $gl-link-color;
}
}
/**
* Actions for Discussions/Notes
*/
......@@ -209,6 +215,17 @@ ul.notes {
color: $notes-action-color;
}
.discussion-actions {
@media (max-width: $screen-sm-max) {
float: none;
margin-left: 0;
.note-action-button {
margin-left: 0;
}
}
}
.note-action-button,
.discussion-action-button {
display: inline-block;
......
/* Generic print styles */
header, nav, nav.main-nav, nav.navbar-collapse, nav.navbar-collapse.collapse {display: none!important;}
.profiler-results {display: none;}
/* Styles targeted specifically at printing files */
.tree-ref-holder, .tree-holder .breadcrumb, .blob-commit-info {display: none;}
.file-title {display: none;}
.file-holder {border: none;}
.wiki h1, .wiki h2, .wiki h3, .wiki h4, .wiki h5, .wiki h6 {margin-top: 17px; }
.wiki h1 {font-size: 30px;}
.wiki h2 {font-size: 22px;}
.wiki h3 {font-size: 18px; font-weight: bold; }
.sidebar-wrapper { display: none; }
.nav { display: none; }
.btn { display: none; }
header,
nav,
nav.main-nav,
nav.navbar-collapse,
nav.navbar-collapse.collapse,
.profiler-results,
.tree-ref-holder,
.tree-holder .breadcrumb,
.blob-commit-info,
.file-title,
.file-holder,
.sidebar-wrapper,
.nav,
.btn,
ul.notes-form,
.merge-request-ci-status .ci-status-link:after,
.issuable-gutter-toggle,
.gutter-toggle,
.issuable-details .content-block-small,
.edit-link,
.note-action-button {
display: none!important;
}
.page-gutter {
padding-top: 0;
padding-left: 0;
}
.right-sidebar {
top: 0;
}
......@@ -20,8 +20,15 @@ class AutocompleteController < ApplicationController
if params[:search].blank?
# Include current user if available to filter by "Me"
if params[:current_user] && current_user
@users = [*@users, current_user].uniq
@users = [*@users, current_user]
end
if params[:author_id].present?
author = User.find_by_id(params[:author_id])
@users = [author, *@users] if author
end
@users.uniq!
end
render json: @users, only: [:name, :username, :id], methods: [:avatar_url]
......
......@@ -51,6 +51,7 @@ class HelpController < ApplicationController
end
def ui
@user = User.new(id: 0, name: 'John Doe', username: '@johndoe')
end
private
......
......@@ -66,8 +66,8 @@ class Projects::IssuesController < Projects::ApplicationController
end
def show
@note = @project.notes.new(noteable: @issue)
@notes = @issue.notes.nonawards.with_associations.fresh
@note = @project.notes.new(noteable: @issue)
@notes = @issue.notes.nonawards.with_associations.fresh
@noteable = @issue
respond_to do |format|
......
......@@ -55,6 +55,15 @@ module IssuablesHelper
h(milestone_title.presence || default_label)
end
def issuable_meta(issuable, project, text)
output = content_tag :strong, "#{text} #{issuable.to_reference}", class: "identifier"
output << " opened #{time_ago_with_tooltip(issuable.created_at)} by".html_safe
output << content_tag(:strong) do
author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "hidden-xs")
author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "hidden-sm hidden-md hidden-lg")
end
end
private
def sidebar_gutter_collapsed?
......
......@@ -5,30 +5,26 @@ module SelectsHelper
css_class << "skip_ldap " if opts[:skip_ldap]
css_class << (opts[:class] || '')
value = opts[:selected] || ''
placeholder = opts[:placeholder] || 'Search for a user'
null_user = opts[:null_user] || false
any_user = opts[:any_user] || false
email_user = opts[:email_user] || false
first_user = opts[:first_user] && current_user ? current_user.username : false
current_user = opts[:current_user] || false
project = opts[:project] || @project
push_code_to_protected_branches = opts[:push_code_to_protected_branches]
html = {
class: css_class,
data: {
placeholder: placeholder,
null_user: null_user,
any_user: any_user,
email_user: email_user,
placeholder: opts[:placeholder] || 'Search for a user',
null_user: opts[:null_user] || false,
any_user: opts[:any_user] || false,
email_user: opts[:email_user] || false,
first_user: first_user,
current_user: current_user,
"push-code-to-protected-branches" => push_code_to_protected_branches
current_user: opts[:current_user] || false,
"push-code-to-protected-branches" => opts[:push_code_to_protected_branches],
author_id: opts[:author_id] || ''
}
}
unless opts[:scope] == :all
project = opts[:project] || @project
if project
html['data-project-id'] = project.id
elsif @group
......
......@@ -366,11 +366,23 @@ module Ci
self.update(erased_by: user, erased_at: Time.now)
end
private
def yaml_variables
global_yaml_variables + job_yaml_variables
end
def global_yaml_variables
if commit.config_processor
commit.config_processor.global_variables.map do |key, value|
{ key: key, value: value, public: true }
end
else
[]
end
end
def job_yaml_variables
if commit.config_processor
commit.config_processor.variables.map do |key, value|
commit.config_processor.job_variables(name).map do |key, value|
{ key: key, value: value, public: true }
end
else
......
......@@ -37,4 +37,10 @@ class ExternalIssue
def to_reference(_from_project = nil)
id
end
def reference_link_text(from_project = nil)
return "##{id}" if /^\d+$/.match(id)
id
end
end
......@@ -90,7 +90,8 @@ class Project < ActiveRecord::Base
after_destroy :remove_pages
after_update :update_forks_visibility_level
after_update :remove_mirror_repository_reference, if: :import_url_changed?
after_update :remove_mirror_repository_reference,
if: ->(project) { project.mirror? && project.import_url_updated? }
ActsAsTaggableOn.strict_case_match = true
acts_as_taggable_on :tags
......@@ -461,6 +462,35 @@ class Project < ActiveRecord::Base
self.import_data.destroy if self.import_data
end
def import_url=(value)
import_url = Gitlab::ImportUrl.new(value)
create_or_update_import_data(credentials: import_url.credentials)
super(import_url.sanitized_url)
end
def import_url
if import_data && super
import_url = Gitlab::ImportUrl.new(super, credentials: import_data.credentials)
import_url.full_url
else
super
end
end
def create_or_update_import_data(data: nil, credentials: nil)
project_import_data = import_data || build_import_data
if data
project_import_data.data ||= {}
project_import_data.data = project_import_data.data.merge(data)
end
if credentials
project_import_data.credentials ||= {}
project_import_data.credentials = project_import_data.credentials.merge(credentials)
end
project_import_data.save
end
def import?
external_import? || forked?
end
......@@ -997,7 +1027,9 @@ class Project < ActiveRecord::Base
def change_head(branch)
repository.before_change_head
gitlab_shell.update_repository_head(self.path_with_namespace, branch)
repository.rugged.references.create('HEAD',
"refs/heads/#{branch}",
force: true)
reload_default_branch
end
......@@ -1218,6 +1250,11 @@ class Project < ActiveRecord::Base
self.merge_requests_ff_only_enabled || self.merge_requests_rebase_enabled
end
def import_url_updated?
# check if import_url has been updated and it's not just the first assignment
import_url_changed? && changes['import_url'].first
end
private
def update_forks_visibility_level
......
......@@ -12,8 +12,20 @@ require 'file_size_validator'
class ProjectImportData < ActiveRecord::Base
belongs_to :project
attr_encrypted :credentials,
key: Gitlab::Application.secrets.db_key_base,
marshal: true,
encode: true,
mode: :per_attribute_iv_and_salt
serialize :data, JSON
validates :project, presence: true
before_validation :symbolize_credentials
def symbolize_credentials
# bang doesn't work here - attr_encrypted makes it not to work
self.credentials = self.credentials.deep_symbolize_keys unless self.credentials.blank?
end
end
......@@ -20,11 +20,13 @@ class Repository
delegate :push_remote_branches, :delete_remote_branches, to: :gitlab_shell
def self.clean_old_archives
repository_downloads_path = Gitlab.config.gitlab.repository_downloads_path
Gitlab::Metrics.measure(:clean_old_archives) do
repository_downloads_path = Gitlab.config.gitlab.repository_downloads_path
return unless File.directory?(repository_downloads_path)
return unless File.directory?(repository_downloads_path)
Gitlab::Popen.popen(%W(find #{repository_downloads_path} -not -path #{repository_downloads_path} -mmin +120 -delete))
Gitlab::Popen.popen(%W(find #{repository_downloads_path} -not -path #{repository_downloads_path} -mmin +120 -delete))
end
end
def initialize(path_with_namespace, project)
......@@ -183,7 +185,12 @@ class Repository
def rm_tag(tag_name)
before_remove_tag
gitlab_shell.rm_tag(path_with_namespace, tag_name)
begin
rugged.tags.delete(tag_name)
true
rescue Rugged::ReferenceError
false
end
end
def add_remote(name, url)
......
module Projects
class ParticipantsService < BaseService
def execute(note_type, note_id)
participating =
if note_type && note_id
participants_in(note_type, note_id)
else
[]
end
def execute(noteable_type, noteable_id)
@noteable_type = noteable_type
@noteable_id = noteable_id
project_members = sorted(project.team.members)
participants = all_members + groups + project_members + participating
participants = target_owner + participants_in_target + all_members + groups + project_members
participants.uniq
end
def participants_in(type, id)
target =
case type
def target
@target ||=
case @noteable_type
when "Issue"
project.issues.find_by_iid(id)
project.issues.find_by_iid(@noteable_id)
when "MergeRequest"
project.merge_requests.find_by_iid(id)
project.merge_requests.find_by_iid(@noteable_id)
when "Commit"
project.commit(id)
project.commit(@noteable_id)
else
nil
end
end
def target_owner
return [] unless target && target.author.present?
[{
name: target.author.name,
username: target.author.username
}]
end
def participants_in_target
return [] unless target
users = target.participants(current_user)
......@@ -30,13 +39,13 @@ module Projects
end
def sorted(users)
users.uniq.to_a.compact.sort_by(&:username).map do |user|
users.uniq.to_a.compact.sort_by(&:username).map do |user|
{ username: user.username, name: user.name }
end
end
def groups
current_user.authorized_groups.sort_by(&:path).map do |group|
current_user.authorized_groups.sort_by(&:path).map do |group|
count = group.users.count
{ username: group.path, name: group.name, count: count }
end
......
......@@ -6,18 +6,17 @@
.login-heading
%h3 Create an account
.login-body
- user = params[:user].present? ? params[:user] : {}
= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f|
.devise-errors
= devise_error_messages!
%div
= f.text_field :name, class: "form-control top", value: user[:name], placeholder: "Name", required: true
= f.text_field :name, class: "form-control top", placeholder: "Name", required: true
%div
= f.text_field :username, class: "form-control middle", value: user[:username], placeholder: "Username", required: true
= f.text_field :username, class: "form-control middle", placeholder: "Username", required: true
%div
= f.email_field :email, class: "form-control middle", value: user[:email], placeholder: "Email", required: true
= f.email_field :email, class: "form-control middle", placeholder: "Email", required: true
.form-group.append-bottom-20#password-strength
= f.password_field :password, class: "form-control bottom", value: user[:password], id: "user_password_sign_up", placeholder: "Password", required: true
= f.password_field :password, class: "form-control bottom", id: "user_password_sign_up", placeholder: "Password", required: true
%div
- if current_application_settings.recaptcha_enabled
= recaptcha_tags
......
......@@ -345,11 +345,11 @@
%ul
%li
%a.dropdown-menu-user-link.is-active{href: "#"}
= link_to_member_avatar(current_user, size: 30)
= link_to_member_avatar(@user, size: 30)
%strong.dropdown-menu-user-full-name
= current_user.name
= @user.name
.dropdown-menu-user-username
= current_user.to_reference
= @user.to_reference
.example
%div
......@@ -372,11 +372,11 @@
%ul
%li
%a.dropdown-menu-user-link.is-active{href: "#"}
= link_to_member_avatar(current_user, size: 30)
= link_to_member_avatar(@user, size: 30)
%strong.dropdown-menu-user-full-name
= current_user.name
= @user.name
.dropdown-menu-user-username
= current_user.to_reference
= @user.to_reference
.dropdown-page-two
.dropdown-title
%button.dropdown-title-button.dropdown-menu-back{aria: {label: "Go back"}}
......
......@@ -20,10 +20,10 @@
job.attr("id", "project_#{@project.id}")
target_field = job.find(".import-target")
target_field.empty()
target_field.append('<strong>#{link_to @project.path_with_namespace, namespace_project_path(@project.namespace, @project)}</strong>')
target_field.append('#{link_to @project.path_with_namespace, namespace_project_path(@project.namespace, @project)}')
$("table.import-jobs tbody").prepend(job)
job.addClass("active").find(".import-actions").html("<i class='fa fa-spinner fa-spin'></i> started")
- else
:plain
job = $("tr#repo_#{@repo_id}")
job.find(".import-actions").html("<i class='fa fa-exclamation-circle'> Error saving project: #{escape_javascript(@project.errors.full_messages.join(','))}</i>")
job.find(".import-actions").html("<i class='fa fa-exclamation-circle'></i> Error saving project: #{escape_javascript(@project.errors.full_messages.join(','))}")
......@@ -10,13 +10,19 @@
%hr
%p
- if @incompatible_repos.any?
= button_tag 'Import all compatible projects', class: "btn btn-success js-import-all"
= button_tag class: "btn btn-import btn-success js-import-all" do
Import all compatible projects
= icon("spinner spin", class: "loading-icon")
- else
= button_tag 'Import all projects', class: "btn btn-success js-import-all"
= button_tag class: "btn btn-success js-import-all" do
Import all projects
= icon("spinner spin", class: "loading-icon")
.table-holder
.table-responsive
%table.table.import-jobs
%colgroup.import-jobs-from-col
%colgroup.import-jobs-to-col
%colgroup.import-jobs-status-col
%thead
%tr
%th From Bitbucket
......@@ -28,7 +34,7 @@
%td
= link_to project.import_source, "https://bitbucket.org/#{project.import_source}", target: "_blank"
%td
%strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
%td.job-status
- if project.import_status == 'finished'
%span
......@@ -47,7 +53,9 @@
%td.import-target
= "#{repo["owner"]}/#{repo["slug"]}"
%td.import-actions.job-status
= button_tag "Import", class: "btn js-add-to-import"
= button_tag class: "btn btn-import js-add-to-import" do
Import
= icon("spinner spin", class: "loading-icon")
- @incompatible_repos.each do |repo|
%tr{id: "repo_#{repo["owner"]}___#{repo["slug"]}"}
%td
......
......@@ -13,10 +13,15 @@
how FogBugz email addresses and usernames are imported into GitLab.
%hr
%p
= button_tag 'Import all projects', class: 'btn btn-success js-import-all'
= button_tag class: 'btn btn-import btn-success js-import-all' do
Import all projects
= icon("spinner spin", class: "loading-icon")
.table-holder
.table-responsive
%table.table.import-jobs
%colgroup.import-jobs-from-col
%colgroup.import-jobs-to-col
%colgroup.import-jobs-status-col
%thead
%tr
%th From FogBugz
......@@ -28,7 +33,7 @@
%td
= project.import_source
%td
%strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
%td.job-status
- if project.import_status == 'finished'
%span
......@@ -47,7 +52,9 @@
%td.import-target
= "#{current_user.username}/#{repo.name}"
%td.import-actions.job-status
= button_tag "Import", class: "btn js-add-to-import"
= button_tag class: "btn btn-import js-add-to-import" do
Import
= icon("spinner spin", class: "loading-icon")
:javascript
new ImporterStatus("#{jobs_import_fogbugz_path}", "#{import_fogbugz_path}");
......@@ -8,10 +8,15 @@
Select projects you want to import.
%hr
%p
= button_tag 'Import all projects', class: "btn btn-success js-import-all"
= button_tag class: "btn btn-import btn-success js-import-all" do
Import all projects
= icon("spinner spin", class: "loading-icon")
.table-holder
.table-responsive
%table.table.import-jobs
%colgroup.import-jobs-from-col
%colgroup.import-jobs-to-col
%colgroup.import-jobs-status-col
%thead
%tr
%th From GitHub
......@@ -23,7 +28,7 @@
%td
= link_to project.import_source, "https://github.com/#{project.import_source}", target: "_blank"
%td
%strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
%td.job-status
- if project.import_status == 'finished'
%span
......@@ -42,7 +47,9 @@
%td.import-target
= repo.full_name
%td.import-actions.job-status
= button_tag "Import", class: "btn js-add-to-import"
= button_tag class: "btn btn-import js-add-to-import" do
Import
= icon("spinner spin", class: "loading-icon")
:javascript
new ImporterStatus("#{jobs_import_github_path}", "#{import_github_path}");
......@@ -8,10 +8,15 @@
Select projects you want to import.
%hr
%p
= button_tag 'Import all projects', class: "btn btn-success js-import-all"
= button_tag class: "btn btn-import btn-success js-import-all" do
Import all projects
= icon("spinner spin", class: "loading-icon")
.table-holder
.table-responsive
%table.table.import-jobs
%colgroup.import-jobs-from-col
%colgroup.import-jobs-to-col
%colgroup.import-jobs-status-col
%thead
%tr
%th From GitLab.com
......@@ -23,7 +28,7 @@
%td
= link_to project.import_source, "https://gitlab.com/#{project.import_source}", target: "_blank"
%td
%strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
%td.job-status
- if project.import_status == 'finished'
%span
......@@ -42,7 +47,9 @@
%td.import-target
= repo["path_with_namespace"]
%td.import-actions.job-status
= button_tag "Import", class: "btn js-add-to-import"
= button_tag class: "btn js-add-to-import" do
Import
= icon("spinner spin", class: "loading-icon")
:javascript
new ImporterStatus("#{jobs_import_gitlab_path}", "#{import_gitlab_path}");
......@@ -8,10 +8,15 @@
Select projects you want to import.
%hr
%p
= button_tag 'Import all projects', class: "btn btn-success js-import-all"
= button_tag class: "btn btn-import btn-success js-import-all" do
Import all projects
= icon("spinner spin", class: "loading-icon")
.table-holder
.table-responsive
%table.table.import-jobs
%colgroup.import-jobs-from-col
%colgroup.import-jobs-to-col
%colgroup.import-jobs-status-col
%thead
%tr
%th From Gitorious.org
......@@ -23,7 +28,7 @@
%td
= link_to project.import_source, "https://gitorious.org/#{project.import_source}", target: "_blank"
%td
%strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
%td.job-status
- if project.import_status == 'finished'
%span
......@@ -42,7 +47,9 @@
%td.import-target
= repo.full_name
%td.import-actions.job-status
= button_tag "Import", class: "btn js-add-to-import"
= button_tag class: "btn btn-import js-add-to-import" do
Import
= icon("spinner spin", class: "loading-icon")
:javascript
new ImporterStatus("#{jobs_import_gitorious_path}", "#{import_gitorious_path}");
......@@ -14,12 +14,19 @@
%hr
%p
- if @incompatible_repos.any?
= button_tag 'Import all compatible projects', class: "btn btn-success js-import-all"
= button_tag class: "btn btn-import btn-success js-import-all" do
Import all compatible projects
= icon("spinner spin", class: "loading-icon")
- else
= button_tag 'Import all projects', class: "btn btn-success js-import-all"
= button_tag class: "btn btn-import btn-success js-import-all" do
Import all projects
= icon("spinner spin", class: "loading-icon")
.table-holder
.table-responsive
%table.table.import-jobs
%colgroup.import-jobs-from-col
%colgroup.import-jobs-to-col
%colgroup.import-jobs-status-col
%thead
%tr
%th From Google Code
......@@ -31,7 +38,7 @@
%td
= link_to project.import_source, "https://code.google.com/p/#{project.import_source}", target: "_blank"
%td
%strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
%td.job-status
- if project.import_status == 'finished'
%span
......@@ -50,7 +57,9 @@
%td.import-target
= "#{current_user.username}/#{repo.name}"
%td.import-actions.job-status
= button_tag "Import", class: "btn js-add-to-import"
= button_tag class: "btn btn-import js-add-to-import" do
Import
= icon("spinner spin", class: "loading-icon")
- @incompatible_repos.each do |repo|
%tr{id: "repo_#{repo.id}"}
%td
......
......@@ -52,6 +52,9 @@
%li
phpunit --coverage-text --colors=never (PHP) -
%code ^\s*Lines:\s*\d+.\d+\%
%li
gcovr (C/C++) -
%code ^TOTAL.*\s+(\d+\%)$
.form-group
.col-sm-offset-2.col-sm-10
......
.md-area
.md-header
%ul.nav-links
%ul.nav-links.clearfix
%li.active
%a.js-md-write-button{ href: "#md-write-holder", tabindex: -1 }
Write
......
......@@ -11,7 +11,7 @@
= cache(cache_key) do
%li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" }
.commit-row-title
%span.item-title.str-truncated
%span.item-title
= link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message"
- if commit.description?
%a.text-expander.js-toggle-button ...
......
- page_title "#{@issue.title} (##{@issue.iid})", "Issues"
- page_description @issue.description
- page_card_attributes @issue.card_attributes
- header_title project_title(@project, "Issues", namespace_project_issues_path(@project.namespace, @project))
= render "header_title"
.clearfix.detail-page-header
.issuable-header
.issuable-status-box.status-box.status-box-closed{ class: issue_button_visibility(@issue, false) }
= icon('check', class: "hidden-sm hidden-md hidden-lg")
%span.hidden-xs
Closed
.issuable-status-box.status-box.status-box-open{ class: issue_button_visibility(@issue, true) }
= icon('circle-o', class: "hidden-sm hidden-md hidden-lg")
%span.hidden-xs Open
.issue
.detail-page-header.issuable-header
.pull-left
.status-box{ class: "status-box-closed #{issue_button_visibility(@issue, false)}"}
%span.hidden-xs
Closed
%span.hidden-sm.hidden-md.hidden-lg
= icon('check')
.status-box{ class: "status-box-open #{issue_button_visibility(@issue, true)}"}
%span.hidden-xs
Open
%span.hidden-sm.hidden-md.hidden-lg
= icon('circle-o')
%a.btn.btn-default.pull-right.visible-xs-block.gutter-toggle.js-sidebar-toggle{ href: "#" }
%a.btn.btn-default.pull-right.visible-xs-block.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" }
= icon('angle-double-left')
.issue-meta
.issuable-meta
= confidential_icon(@issue)
%strong.identifier
Issue ##{@issue.iid}
%span.creator
opened
.editor-details
.editor-details
= time_ago_with_tooltip(@issue.created_at)
by
%strong
= link_to_member(@project, @issue.author, size: 24, mobile_classes: "hidden-xs")
%strong
= link_to_member(@project, @issue.author, size: 24, mobile_classes: "hidden-sm hidden-md hidden-lg",
by_username: true, avatar: false)
= issuable_meta(@issue, @project, "Issue")
- if can?(current_user, :create_issue, @project) || can?(current_user, :update_issue, @issue)
.issuable-actions
.clearfix.issue-btn-group.dropdown
%button.btn.btn-default.pull-left.hidden-md.hidden-lg{ data: { toggle: "dropdown" } }
%span.caret
Options
.dropdown-menu.dropdown-menu-align-right.hidden-lg
%ul
- if can?(current_user, :create_issue, @project)
%li
= link_to 'New issue', new_namespace_project_issue_path(@project.namespace, @project), title: 'New issue', id: 'new_issue_link'
- if can?(current_user, :update_issue, @issue)
%li
= link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
%li
= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
%li
= link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue)
- if can?(current_user, :create_issue, @project)
= link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-nr btn-grouped new-issue-link btn-success', title: 'New issue', id: 'new_issue_link' do
= icon('plus')
New issue
- if can?(current_user, :update_issue, @issue)
= link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-nr btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-nr btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
= link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-nr btn-grouped issuable-edit' do
= icon('pencil-square-o')
Edit
.pull-right.issue-btn-group
- if can?(current_user, :create_issue, @project)
= link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'btn btn-nr btn-grouped new-issue-link btn-success', title: 'New issue', id: 'new_issue_link' do
= icon('plus')
New issue
- if can?(current_user, :update_issue, @issue)
= link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
= link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
= link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'btn btn-nr btn-grouped issuable-edit' do
= icon('pencil-square-o')
Edit
.issue-details.issuable-details
.detail-page-description.content-block
%h2.title
= markdown escape_once(@issue.title), pipeline: :single_line
%div
- if @issue.description.present?
.description{class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : ''}
.wiki
= preserve do
= markdown(@issue.description, cache_key: [@issue, "description"])
%textarea.hidden.js-task-list-field
= @issue.description
= edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago')
.issue-details.issuable-details
.detail-page-description.content-block
%h2.title
= markdown escape_once(@issue.title), pipeline: :single_line
- if @issue.description.present?
.description{ class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '' }
.wiki
= preserve do
= markdown(@issue.description, cache_key: [@issue, "description"])
%textarea.hidden.js-task-list-field
= @issue.description
= edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago')
#merge-requests{'data-url' => referenced_merge_requests_namespace_project_issue_url(@project.namespace, @project, @issue)}
// This element is filled in using JavaScript.
#merge-requests{ data: { url: referenced_merge_requests_namespace_project_issue_url(@project.namespace, @project, @issue) } }
// This element is filled in using JavaScript.
#related-branches{'data-url' => related_branches_namespace_project_issue_url(@project.namespace, @project, @issue)}
// This element is filled in using JavaScript.
#related-branches{ data: { url: related_branches_namespace_project_issue_url(@project.namespace, @project, @issue) } }
// This element is filled in using JavaScript.
.content-block.content-block-small
= render 'new_branch'
= render 'votes/votes_block', votable: @issue
.content-block.content-block-small
= render 'new_branch'
= render 'votes/votes_block', votable: @issue
.row
%section.col-md-12
.issuable-discussion
= render 'projects/issues/discussion'
%section.issuable-discussion
= render 'projects/issues/discussion'
= render 'shared/issuable/sidebar', issuable: @issue
$('aside.right-sidebar')[0].outerHTML = "#{escape_javascript(render 'shared/issuable/sidebar', issuable: @issue)}";
$('aside.right-sidebar').effect('highlight');
new IssuableContext();
......@@ -16,11 +16,9 @@
= dropdown_title("Select source project")
= dropdown_filter("Search projects")
= dropdown_content do
- is_active = f.object.source_project_id == @merge_request.source_project.id
%ul
%li
%a{ href: "#", class: "#{("is-active" if is_active)}", data: { id: @merge_request.source_project.id } }
= @merge_request.source_project_path
= render 'projects/merge_requests/dropdowns/project',
projects: [@merge_request.source_project],
selected: f.object.source_project_id
.merge-request-select.dropdown
= f.hidden_field :source_branch
= dropdown_toggle "Select source branch", { toggle: "dropdown", field_name: "#{f.object_name}[source_branch]" }, { toggle_class: "js-compare-dropdown js-source-branch" }
......@@ -28,11 +26,9 @@
= dropdown_title("Select source branch")
= dropdown_filter("Search branches")
= dropdown_content do
%ul
- @merge_request.source_branches.each do |branch|
%li
%a{ href: "#", class: "#{("is-active" if f.object.source_branch == branch)}", data: { id: branch } }
= branch
= render 'projects/merge_requests/dropdowns/branch',
branches: @merge_request.source_branches,
selected: f.object.source_branch
.panel-footer
= icon('spinner spin', class: 'js-source-loading')
%ul.list-unstyled.mr_source_commit
......@@ -50,11 +46,9 @@
= dropdown_title("Select target project")
= dropdown_filter("Search projects")
= dropdown_content do
%ul
- projects.each do |project|
%li
%a{ href: "#", class: "#{("is-active" if f.object.target_project_id == project.id)}", data: { id: project.id } }
= project.path_with_namespace
= render 'projects/merge_requests/dropdowns/project',
projects: projects,
selected: f.object.target_project_id
.merge-request-select.dropdown
= f.hidden_field :target_branch
= dropdown_toggle f.object.target_branch, { toggle: "dropdown", field_name: "#{f.object_name}[target_branch]" }, { toggle_class: "js-compare-dropdown js-target-branch" }
......@@ -62,11 +56,9 @@
= dropdown_title("Select target branch")
= dropdown_filter("Search branches")
= dropdown_content do
%ul
- @merge_request.target_branches.each do |branch|
%li
%a{ href: "#", class: "#{("is-active" if f.object.target_branch == branch)}", data: { id: branch } }
= branch
= render 'projects/merge_requests/dropdowns/branch',
branches: @merge_request.target_branches,
selected: f.object.target_branch
.panel-footer
= icon('spinner spin', class: "js-target-loading")
%ul.list-unstyled.mr_target_commit
......
- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests"
- page_description @merge_request.description
- page_card_attributes @merge_request.card_attributes
= render "header_title"
- header_title project_title(@project, "Merge Requests", namespace_project_merge_requests_path(@project.namespace, @project))
- if params[:view] == 'parallel'
- fluid_layout true
......@@ -32,8 +31,7 @@
%span Request to merge
%span.label-branch= source_branch_with_namespace(@merge_request)
%span into
= link_to namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch" do
= @merge_request.target_branch
= link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"
- if @merge_request.open? && @merge_request.diverged_from_target_branch?
%span (#{pluralize(@merge_request.diverged_commits_count, 'commit')} behind)
......
%ul
- branches.each do |branch|
%li
%a{ href: '#', class: "#{('is-active' if selected == branch)}", data: { id: branch } }
= branch
%ul
- projects.each do |project|
%li
%a{ href: "#", class: "#{('is-active' if selected == project.id)}", data: { id: project.id } }
= project.path_with_namespace
.detail-page-header
.status-box{ class: status_box_class(@merge_request) }
%span.hidden-xs
= @merge_request.state_human_name
%span.hidden-sm.hidden-md.hidden-lg
= icon(@merge_request.state_icon_name)
%a.btn.btn-default.pull-right.visible-xs-block.gutter-toggle.js-sidebar-toggle{ href: "#" }
= icon('angle-double-left')
.issue-meta
%strong.identifier
%span.hidden-sm.hidden-md.hidden-lg
MR
.clearfix.detail-page-header
.issuable-header
.issuable-status-box.status-box{ class: status_box_class(@merge_request) }
= icon(@merge_request.state_icon_name, class: "hidden-sm hidden-md hidden-lg")
%span.hidden-xs
Merge Request
!#{@merge_request.iid}
%span.creator
opened
.editor-details
= time_ago_with_tooltip(@merge_request.created_at)
by
%strong
= link_to_member(@project, @merge_request.author, size: 24, mobile_classes: "hidden-xs")
%strong
= link_to_member(@project, @merge_request.author, size: 24, mobile_classes: "hidden-sm hidden-md hidden-lg",
by_username: true, avatar: false)
= @merge_request.state_human_name
.issue-btn-group.pull-right
- if can?(current_user, :update_merge_request, @merge_request)
- if @merge_request.open?
= link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: 'btn btn-nr btn-grouped btn-close', title: 'Close merge request'
= link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn btn-nr btn-grouped issuable-edit', id: 'edit_merge_request' do
%a.btn.btn-default.pull-right.visible-xs-block.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" }
= icon('angle-double-left')
.issuable-meta
= issuable_meta(@merge_request, @project, "Merge Request")
- if can?(current_user, :update_merge_request, @merge_request)
.issuable-actions
.clearfix.issue-btn-group.dropdown
%button.btn.btn-default.pull-left.hidden-md.hidden-lg{ data: { toggle: "dropdown" } }
%span.caret
Options
.dropdown-menu.dropdown-menu-align-right.hidden-lg
%ul
%li{ class: issue_button_visibility(@merge_request, true) }
= link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, title: 'Close merge request'
%li{ class: issue_button_visibility(@merge_request, false) }
= link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request'
%li
= link_to 'Edit', edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'issuable-edit'
= link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: "hidden-xs hidden-sm btn btn-nr btn-grouped btn-close #{issue_button_visibility(@merge_request, true)}", title: 'Close merge request'
= link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "hidden-xs hidden-sm btn btn-nr btn-grouped btn-reopen reopen-mr-link #{issue_button_visibility(@merge_request, false)}", title: 'Reopen merge request'
= link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "hidden-xs hidden-sm btn btn-nr btn-grouped issuable-edit" do
= icon('pencil-square-o')
Edit
- if @merge_request.closed?
= link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'btn btn-nr btn-grouped btn-reopen reopen-mr-link', title: 'Reopen merge request'
$('aside.right-sidebar')[0].outerHTML = "#{escape_javascript(render 'shared/issuable/sidebar', issuable: @merge_request)}";
$('aside.right-sidebar').effect('highlight');
new IssuableContext();
%ul
- @target_branches.each do |branch|
%li
%a{ href: "#", class: "#{("is-active" if "a" == branch)}", data: { id: branch } }
= branch
= render 'projects/merge_requests/dropdowns/branch',
branches: @target_branches,
selected: nil
- note = discussion_notes.first
.timeline-entry
%li.note.note-discussion.timeline-entry
.timeline-entry-inner
.timeline-icon
= link_to user_path(note.author) do
......
%li.timeline-entry{ id: dom_id(note), class: [dom_class(note), "note-row-#{note.id}", ('system-note' if note.system)] }
- note_editable = note_editable?(note)
%li.timeline-entry{ id: dom_id(note), class: [dom_class(note), "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable} }
.timeline-entry-inner
.timeline-icon
%a{href: user_path(note.author)}
......@@ -15,16 +16,16 @@
- if access
%span.note-role
= access
- if note_editable?(note)
- if note_editable
= link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
= icon('pencil')
= 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')
.note-body{class: note_editable?(note) ? 'js-task-list-container' : ''}
.note-body{class: note_editable ? 'js-task-list-container' : ''}
.note-text
= preserve do
= markdown(note.note, pipeline: :note, cache_key: [note, "note"])
- if note_editable?(note)
- if note_editable
= render 'projects/notes/edit_form', note: note
= edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true)
......
%ul#notes-list.notes.main-notes-list.timeline
= render "projects/notes/notes"
%ul.notes.timeline
%ul.notes.notes-form.timeline
%li.timeline-entry
- if can? current_user, :create_note, @project
.timeline-icon.hidden-xs.hidden-sm
......
......@@ -6,15 +6,11 @@
= "#{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
= time_ago_with_tooltip(note.created_at, placement: "bottom", html_class: "discussion_updated_ago")
.discussion-actions
= link_to "#", class: "discussion-action-button discussion-toggle-button js-toggle-button" do
%i.fa.fa-chevron-up
Show/hide discussion
.last-update.hide.js-toggle-content
- last_note = discussion_notes.last
last updated by
= link_to_member(@project, last_note.author, avatar: false)
#{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
......@@ -8,21 +8,18 @@
= "#{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')
= time_ago_with_tooltip(note.created_at, placement: "bottom", html_class: "discussion_updated_ago")
.discussion-actions
= link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do
%i.fa.fa-chevron-up
Show/hide discussion
.last-update.hide.js-toggle-content
- last_note = discussion_notes.last
last updated by
= link_to_member(@project, last_note.author, avatar: false)
#{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
- else
.panel.panel-default
.notes{ data: { discussion_id: discussion_notes.first.discussion_id } }
= render discussion_notes
%ul.notes.timeline
= render discussion_notes
.discussion-reply-holder
= link_to_reply_diff(discussion_notes.first)
......@@ -5,14 +5,10 @@
.inline.discussion-headline-light
= "#{note.author.to_reference} started a discussion"
on the outdated diff
= time_ago_with_tooltip(note.created_at, placement: "bottom", html_class: "discussion_updated_ago")
.discussion-actions
= link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do
%i.fa.fa-chevron-down
Show/hide discussion
.last-update.hide.js-toggle-content
- last_note = discussion_notes.last
last updated by
= link_to_member(@project, last_note.author, avatar: false)
#{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
......@@ -4,7 +4,7 @@
= f.label :title, class: 'control-label'
.col-sm-10
= f.text_field :title, maxlength: 255, autofocus: true, autocomplete: 'off',
class: 'form-control pad js-gfm-input', required: true
class: 'form-control pad', required: true
- if issuable.is_a?(MergeRequest)
%p.help-block
......
......@@ -49,7 +49,7 @@
.selectbox.hide-collapsed
= f.hidden_field 'assignee_id', value: issuable.assignee_id, id: 'issue_assignee_id'
= dropdown_tag('Select assignee', options: { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), field_name: "#{issuable.to_ability_name}[assignee_id]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true } })
= dropdown_tag('Select assignee', options: { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), author_id: issuable.author_id, field_name: "#{issuable.to_ability_name}[assignee_id]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true } })
.block.milestone
.sidebar-collapsed-icon
......
class AddImportCredentialsToProjectImportData < ActiveRecord::Migration
def change
add_column :project_import_data, :encrypted_credentials, :text
add_column :project_import_data, :encrypted_credentials_iv, :string
add_column :project_import_data, :encrypted_credentials_salt, :string
end
end
# Loops through old importer projects that kept a token/password in the import URL
# and encrypts the credentials into a separate field in project#import_data
# #down method not supported
class RemoveWrongImportUrlFromProjects < ActiveRecord::Migration
class ProjectImportDataFake
extend AttrEncrypted
attr_accessor :credentials
attr_encrypted :credentials, key: Gitlab::Application.secrets.db_key_base, marshal: true, encode: true, :mode => :per_attribute_iv_and_salt
end
def up
say("Encrypting and migrating project import credentials...")
# This should cover GitHub, GitLab, Bitbucket user:password, token@domain, and other similar URLs.
in_transaction(message: "Projects including GitHub and GitLab projects with an unsecured URL.") { process_projects_with_wrong_url }
in_transaction(message: "Migrating Bitbucket credentials...") { process_project(import_type: 'bitbucket', credentials_keys: ['bb_session']) }
in_transaction(message: "Migrating FogBugz credentials...") { process_project(import_type: 'fogbugz', credentials_keys: ['fb_session']) }
end
def process_projects_with_wrong_url
projects_with_wrong_import_url.each do |project|
begin
import_url = Gitlab::ImportUrl.new(project["import_url"])
update_import_url(import_url, project)
update_import_data(import_url, project)
rescue URI::InvalidURIError
nullify_import_url(project)
end
end
end
def process_project(import_type:, credentials_keys: [])
unencrypted_import_data(import_type: import_type).each do |data|
replace_data_credentials(data, credentials_keys)
end
end
def replace_data_credentials(data, credentials_keys)
data_hash = JSON.load(data['data']) if data['data']
unless data_hash.blank?
encrypted_data_hash = encrypt_data(data_hash, credentials_keys)
unencrypted_data = data_hash.empty? ? ' NULL ' : quote(data_hash.to_json)
update_with_encrypted_data(encrypted_data_hash, data['id'], unencrypted_data)
end
end
def encrypt_data(data_hash, credentials_keys)
new_data_hash = {}
credentials_keys.each do |key|
new_data_hash[key.to_sym] = data_hash.delete(key) if data_hash[key]
end
new_data_hash.deep_symbolize_keys
end
def in_transaction(message:)
say_with_time(message) do
ActiveRecord::Base.transaction do
yield
end
end
end
def update_import_data(import_url, project)
fake_import_data = ProjectImportDataFake.new
fake_import_data.credentials = import_url.credentials
import_data_id = project['import_data_id']
if import_data_id
execute(update_import_data_sql(import_data_id, fake_import_data))
else
execute(insert_import_data_sql(project['id'], fake_import_data))
end
end
def update_with_encrypted_data(data_hash, import_data_id, unencrypted_data = ' NULL ')
fake_import_data = ProjectImportDataFake.new
fake_import_data.credentials = data_hash
execute(update_import_data_sql(import_data_id, fake_import_data, unencrypted_data))
end
def update_import_url(import_url, project)
execute("UPDATE projects SET import_url = #{quote(import_url.sanitized_url)} WHERE id = #{project['id']}")
end
def nullify_import_url(project)
execute("UPDATE projects SET import_url = NULL WHERE id = #{project['id']}")
end
def insert_import_data_sql(project_id, fake_import_data)
%(
INSERT INTO project_import_data
(encrypted_credentials,
project_id,
encrypted_credentials_iv,
encrypted_credentials_salt)
VALUES ( #{quote(fake_import_data.encrypted_credentials)},
'#{project_id}',
#{quote(fake_import_data.encrypted_credentials_iv)},
#{quote(fake_import_data.encrypted_credentials_salt)})
).squish
end
def update_import_data_sql(id, fake_import_data, data = 'NULL')
%(
UPDATE project_import_data
SET encrypted_credentials = #{quote(fake_import_data.encrypted_credentials)},
encrypted_credentials_iv = #{quote(fake_import_data.encrypted_credentials_iv)},
encrypted_credentials_salt = #{quote(fake_import_data.encrypted_credentials_salt)},
data = #{data}
WHERE id = '#{id}'
).squish
end
#GitHub projects with token, and any user:password@ based URL
def projects_with_wrong_import_url
select_all("SELECT p.id, p.import_url, i.id as import_data_id FROM projects p LEFT JOIN project_import_data i on p.id = i.project_id WHERE p.import_url <> '' AND p.import_url LIKE '%//%@%'")
end
# All imports with data for import_type
def unencrypted_import_data(import_type: )
select_all("SELECT i.id, p.import_url, i.data FROM projects p INNER JOIN project_import_data i ON p.id = i.project_id WHERE p.import_url <> '' AND p.import_type = '#{import_type}' ")
end
def quote(value)
ActiveRecord::Base.connection.quote(value)
end
end
......@@ -807,6 +807,9 @@ ActiveRecord::Schema.define(version: 20160414064845) do
create_table "project_import_data", force: :cascade do |t|
t.integer "project_id"
t.text "data"
t.text "encrypted_credentials"
t.string "encrypted_credentials_iv"
t.string "encrypted_credentials_salt"
end
create_table "projects", force: :cascade do |t|
......
......@@ -13,7 +13,7 @@ GitLab offers a [continuous integration][ci] service. If you
and configure your GitLab project to use a [Runner], then each merge request or
push triggers a build.
The `.gitlab-ci.yml` file tells the GitLab runner what do to. By default it
The `.gitlab-ci.yml` file tells the GitLab runner what to do. By default it
runs three [stages]: `build`, `test`, and `deploy`.
If everything runs OK (no non-zero return values), you'll get a nice green
......
## Variables
When receiving a build from GitLab CI, the runner prepares the build environment.
It starts by setting a list of **predefined variables** (Environment Variables) and a list of **user-defined variables**
The variables can be overwritten. They take precedence over each other in this order:
1. Trigger variables
1. Secure variables
1. YAML-defined variables
1. YAML-defined job-level variables
1. YAML-defined global variables
1. Predefined variables
For example, if you define:
1. API_TOKEN=SECURE as Secure Variable
1. API_TOKEN=YAML as YAML-defined variable
1. `API_TOKEN=SECURE` as Secure Variable
1. `API_TOKEN=YAML` as YAML-defined variable
The API_TOKEN will take the Secure Variable value: `SECURE`.
The `API_TOKEN` will take the Secure Variable value: `SECURE`.
### Predefined variables (Environment Variables)
......@@ -70,15 +73,20 @@ These variables can be later used in all executed commands and scripts.
The YAML-defined variables are also set to all created service containers, thus allowing to fine tune them.
Variables can be defined at a global level, but also at a job level.
More information about Docker integration can be found in [Using Docker Images](../docker/using_docker_images.md).
### User-defined variables (Secure Variables)
**This feature requires GitLab Runner 0.4.0 or higher**
GitLab CI allows you to define per-project **Secure Variables** that are set in build environment.
GitLab CI allows you to define per-project **Secure Variables** that are set in
the build environment.
The secure variables are stored out of the repository (the `.gitlab-ci.yml`).
The variables are securely passed to GitLab Runner and are available in build environment.
It's desired method to use them for storing passwords, secret keys or whatever you want.
The variables are securely passed to GitLab Runner and are available in the
build environment.
It's desired method to use them for storing passwords, secret keys or whatever
you want.
**The value of the variable can be shown in build log if explicitly asked to do so.**
If your project is public or internal you can make the builds private.
......
......@@ -23,6 +23,7 @@ If you want a quick introduction to GitLab CI, follow our
- [Jobs](#jobs)
- [script](#script)
- [stage](#stage)
- [job variables](#job-variables)
- [only and except](#only-and-except)
- [tags](#tags)
- [when](#when)
......@@ -174,6 +175,8 @@ These variables can be later used in all executed commands and scripts.
The YAML-defined variables are also set to all created service containers,
thus allowing to fine tune them.
Variables can be also defined on [job level](#job-variables).
### cache
>**Note:**
......@@ -324,6 +327,7 @@ job_name:
| services | no | Use docker services, covered in [Using Docker Images](../docker/using_docker_images.md#define-image-and-services-from-gitlab-ciyml) |
| stage | no | Defines a build stage (default: `test`) |
| type | no | Alias for `stage` |
| variables | no | Define build variables on a job level |
| only | no | Defines a list of git refs for which build is created |
| except | no | Defines a list of git refs for which build is not created |
| tags | no | Defines a list of tags which are used to select Runner |
......@@ -414,6 +418,18 @@ job:
The above example will run `job` for all branches on `gitlab-org/gitlab-ce`,
except master.
### job variables
It is possible to define build variables using a `variables` keyword on a job
level. It works basically the same way as its global-level equivalent but
allows you to define job-specific build variables.
When the `variables` keyword is used on a job level, it overrides global YAML
build variables and predefined variables.
Build variables priority is defined in
[variables documentation](../variables/README.md).
### tags
`tags` is used to select specific Runners from the list of all Runners that are
......
......@@ -556,8 +556,8 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
click_button "Submit merge request"
page.within '.detail-page-header' do
click_link "Edit"
page.within '.issuable-actions' do
first(:link, 'Edit', visible: true).click
end
page.within 'ul.approver-list' do
......
......@@ -2,7 +2,7 @@ module SharedIssuable
include Spinach::DSL
def edit_issuable
find(:css, '.issuable-edit').click
find('.issuable-edit', visible: true).click
end
step 'project "Community" has "Community issue" open issue' do
......
......@@ -7,9 +7,9 @@ module Ci
ALLOWED_YAML_KEYS = [:before_script, :image, :services, :types, :stages, :variables, :cache]
ALLOWED_JOB_KEYS = [:tags, :script, :only, :except, :type, :image, :services,
:allow_failure, :type, :stage, :when, :artifacts, :cache,
:dependencies]
:dependencies, :variables]
attr_reader :before_script, :image, :services, :variables, :path, :cache
attr_reader :before_script, :image, :services, :path, :cache
def initialize(config, path = nil)
@config = YAML.safe_load(config, [Symbol], [], true)
......@@ -40,6 +40,17 @@ module Ci
@stages || DEFAULT_STAGES
end
def global_variables
@variables
end
def job_variables(name)
job = @jobs[name.to_sym]
return [] unless job
job.fetch(:variables, [])
end
private
def initial_parsing
......@@ -115,7 +126,7 @@ module Ci
end
unless @variables.nil? || validate_variables(@variables)
raise ValidationError, "variables should be a map of key-valued strings"
raise ValidationError, "variables should be a map of key-value strings"
end
if @cache
......@@ -145,6 +156,7 @@ module Ci
validate_job_types!(name, job)
validate_job_stage!(name, job) if job[:stage]
validate_job_variables!(name, job) if job[:variables]
validate_job_cache!(name, job) if job[:cache]
validate_job_artifacts!(name, job) if job[:artifacts]
validate_job_dependencies!(name, job) if job[:dependencies]
......@@ -206,6 +218,13 @@ module Ci
end
end
def validate_job_variables!(name, job)
unless validate_variables(job[:variables])
raise ValidationError,
"#{name} job: variables should be a map of key-value strings"
end
end
def validate_job_cache!(name, job)
if job[:cache][:key] && !validate_string(job[:cache][:key])
raise ValidationError, "#{name} job: cache:key parameter should be a string"
......
......@@ -98,19 +98,6 @@ module Gitlab
"#{path}.git", "#{new_path}.git"])
end
# Update HEAD for repository
#
# path - project path with namespace
# branch - repository branch name
#
# Ex.
# update_repository_head("gitlab/gitlab-ci", "3-1-stable")
#
def update_repository_head(path, branch)
Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'update-head',
"#{path}.git", branch])
end
# Fork repository to new namespace
#
# path - project path with namespace
......@@ -136,33 +123,6 @@ module Gitlab
'rm-project', "#{name}.git"])
end
# Add repository branch from passed ref
#
# path - project path with namespace
# branch_name - new branch name
# ref - HEAD for new branch
#
# Ex.
# add_branch("gitlab/gitlab-ci", "4-0-stable", "master")
#
def add_branch(path, branch_name, ref)
Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'create-branch',
"#{path}.git", branch_name, ref])
end
# Remove repository branch
#
# path - project path with namespace
# branch_name - branch name to remove
#
# Ex.
# rm_branch("gitlab/gitlab-ci", "4-0-stable")
#
def rm_branch(path, branch_name)
Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'rm-branch',
"#{path}.git", branch_name])
end
# Add repository tag from passed ref
#
# path - project path with namespace
......@@ -181,19 +141,6 @@ module Gitlab
Gitlab::Utils.system_silent(cmd)
end
# Remove repository tag
#
# path - project path with namespace
# tag_name - tag name to remove
#
# Ex.
# rm_tag("gitlab/gitlab-ci", "v4.0")
#
def rm_tag(path, tag_name)
Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'rm-tag',
"#{path}.git", tag_name])
end
# Gc repository
#
# path - project path with namespace
......
......@@ -5,6 +5,17 @@ module Gitlab
attr_reader :consumer, :api
def self.from_project(project)
import_data_credentials = project.import_data.credentials if project.import_data
if import_data_credentials && import_data_credentials[:bb_session]
token = import_data_credentials[:bb_session][:bitbucket_access_token]
token_secret = import_data_credentials[:bb_session][:bitbucket_access_token_secret]
new(token, token_secret)
else
raise Projects::ImportService::Error, "Unable to find project import data credentials for project ID: #{@project.id}"
end
end
def initialize(access_token = nil, access_token_secret = nil)
@consumer = ::OAuth::Consumer.new(
config.app_id,
......@@ -54,7 +65,7 @@ module Gitlab
def issues(project_identifier)
all_issues = []
offset = 0
per_page = 50 # Maximum number allowed by Bitbucket
per_page = 50 # Maximum number allowed by Bitbucket
index = 0
begin
......@@ -120,7 +131,7 @@ module Gitlab
end
def config
Gitlab.config.omniauth.providers.find { |provider| provider.name == "bitbucket"}
Gitlab.config.omniauth.providers.find { |provider| provider.name == "bitbucket" }
end
def bitbucket_options
......
......@@ -5,10 +5,7 @@ module Gitlab
def initialize(project)
@project = project
import_data = project.import_data.try(:data)
bb_session = import_data["bb_session"] if import_data
@client = Client.new(bb_session["bitbucket_access_token"],
bb_session["bitbucket_access_token_secret"])
@client = Client.from_project(@project)
@formatter = Gitlab::ImportFormatter.new
end
......
......@@ -6,10 +6,7 @@ module Gitlab
def initialize(project)
@project = project
@current_user = project.creator
import_data = project.import_data.try(:data)
bb_session = import_data["bb_session"] if import_data
@client = Client.new(bb_session["bitbucket_access_token"],
bb_session["bitbucket_access_token_secret"])
@client = Client.from_project(@project)
end
def execute
......
......@@ -23,7 +23,8 @@ module Gitlab
import_url: "ssh://git@bitbucket.org/#{repo["owner"]}/#{repo["slug"]}.git",
).execute
project.create_import_data(data: { "bb_session" => session_data } )
project.create_or_update_import_data(credentials: { bb_session: session_data })
project
end
end
......
......@@ -8,17 +8,17 @@ module Gitlab
import_data = project.import_data.try(:data)
repo_data = import_data['repo'] if import_data
@repo = FogbugzImport::Repository.new(repo_data)
@known_labels = Set.new
if repo_data
@repo = FogbugzImport::Repository.new(repo_data)
@known_labels = Set.new
else
raise Projects::ImportService::Error, "Unable to find project import data credentials for project ID: #{@project.id}"
end
end
def execute
return true unless repo.valid?
data = project.import_data.try(:data)
client = Gitlab::FogbugzImport::Client.new(token: data['fb_session']['token'], uri: data['fb_session']['uri'])
client = Gitlab::FogbugzImport::Client.new(token: fb_session[:token], uri: fb_session[:uri])
@cases = client.cases(@repo.id.to_i)
@categories = client.categories
......@@ -30,6 +30,10 @@ module Gitlab
private
def fb_session
@import_data_credentials ||= project.import_data.credentials[:fb_session] if project.import_data && project.import_data.credentials
end
def user_map
@user_map ||= begin
user_map = Hash.new
......@@ -236,9 +240,8 @@ module Gitlab
end
def build_attachment_url(rel_url)
data = project.import_data.try(:data)
uri = data['fb_session']['uri']
token = data['fb_session']['token']
uri = fb_session[:uri]
token = fb_session[:token]
"#{uri}/#{rel_url}&token=#{token}"
end
......
......@@ -24,13 +24,7 @@ module Gitlab
import_url: Project::UNKNOWN_IMPORT_URL
).execute
project.create_import_data(
data: {
'repo' => repo.raw_data,
'user_map' => user_map,
'fb_session' => fb_session
}
)
project.create_or_update_import_data(data: { 'repo' => repo.raw_data, 'user_map' => user_map }, credentials: { fb_session: fb_session })
project
end
......
......@@ -7,10 +7,12 @@ module Gitlab
def initialize(project)
@project = project
import_data = project.import_data.try(:data)
github_session = import_data["github_session"] if import_data
@client = Client.new(github_session["github_access_token"])
@formatter = Gitlab::ImportFormatter.new
if import_data_credentials
@client = Client.new(import_data_credentials[:user])
@formatter = Gitlab::ImportFormatter.new
else
raise Projects::ImportService::Error, "Unable to find project import data credentials for project ID: #{@project.id}"
end
end
def execute
......@@ -19,6 +21,10 @@ module Gitlab
private
def import_data_credentials
@import_data_credentials ||= project.import_data.credentials if project.import_data
end
def import_issues
client.list_issues(project.import_source, state: :all,
sort: :created,
......
......@@ -11,7 +11,7 @@ module Gitlab
end
def execute
project = ::Projects::CreateService.new(
::Projects::CreateService.new(
current_user,
name: repo.name,
path: repo.name,
......@@ -23,9 +23,6 @@ module Gitlab
import_url: repo.clone_url.sub("https://", "https://#{@session_data[:github_access_token]}@"),
wiki_enabled: !repo.has_wiki? # If repo has wiki we'll import it later
).execute
project.create_import_data(data: { "github_session" => session_data } )
project
end
end
end
......
......@@ -5,10 +5,13 @@ module Gitlab
def initialize(project)
@project = project
import_data = project.import_data.try(:data)
gitlab_session = import_data["gitlab_session"] if import_data
@client = Client.new(gitlab_session["gitlab_access_token"])
@formatter = Gitlab::ImportFormatter.new
credentials = import_data
if credentials && credentials[:password]
@client = Client.new(credentials[:password])
@formatter = Gitlab::ImportFormatter.new
else
raise Projects::ImportService::Error, "Unable to find project import data credentials for project ID: #{@project.id}"
end
end
def execute
......
......@@ -23,7 +23,6 @@ module Gitlab
import_url: repo["http_url_to_repo"].sub("://", "://oauth2:#{@session_data[:gitlab_access_token]}@")
).execute
project.create_import_data(data: { "gitlab_session" => session_data } )
project
end
end
......
......@@ -24,12 +24,7 @@ module Gitlab
import_url: repo.import_url
).execute
project.create_import_data(
data: {
"repo" => repo.raw_data,
"user_map" => user_map
}
)
project.create_or_update_import_data(data: { 'repo' => repo.raw_data, 'user_map' => user_map })
project
end
......
# I'm borrowing this class from: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3066
# So we should fix the conflict once the CE -> EE merge process starts.
module Gitlab
class ImportUrl
def initialize(url, credentials: nil)
@url = URI.parse(url)
@url = URI.parse(URI.encode(url))
@credentials = credentials
end
......
......@@ -11,6 +11,8 @@ module Gitlab
module Instrumentation
SERIES = 'method_calls'
PROXY_IVAR = :@__gitlab_instrumentation_proxy
def self.configure
yield self
end
......@@ -91,6 +93,18 @@ module Gitlab
end
end
# Returns true if a module is instrumented.
#
# mod - The module to check
def self.instrumented?(mod)
mod.instance_variable_defined?(PROXY_IVAR)
end
# Returns the proxy module (if any) of `mod`.
def self.proxy_module(mod)
mod.instance_variable_get(PROXY_IVAR)
end
# Instruments a method.
#
# type - The type (:class or :instance) of method to instrument.
......@@ -99,9 +113,8 @@ module Gitlab
def self.instrument(type, mod, name)
return unless Metrics.enabled?
name = name.to_sym
alias_name = :"_original_#{name}"
target = type == :instance ? mod : mod.singleton_class
name = name.to_sym
target = type == :instance ? mod : mod.singleton_class
if type == :instance
target = mod
......@@ -113,6 +126,12 @@ module Gitlab
method = mod.method(name)
end
unless instrumented?(target)
target.instance_variable_set(PROXY_IVAR, Module.new)
end
proxy_module = self.proxy_module(target)
# Some code out there (e.g. the "state_machine" Gem) checks the arity of
# a method to make sure it only passes arguments when the method expects
# any. If we were to always overwrite a method to take an `*args`
......@@ -125,17 +144,13 @@ module Gitlab
args_signature = '*args, &block'
end
send_signature = "__send__(#{alias_name.inspect}, #{args_signature})"
target.class_eval <<-EOF, __FILE__, __LINE__ + 1
alias_method #{alias_name.inspect}, #{name.inspect}
proxy_module.class_eval <<-EOF, __FILE__, __LINE__ + 1
def #{name}(#{args_signature})
trans = Gitlab::Metrics::Instrumentation.transaction
if trans
start = Time.now
retval = #{send_signature}
retval = super
duration = (Time.now - start) * 1000.0
if duration >= Gitlab::Metrics.method_call_threshold
......@@ -148,10 +163,12 @@ module Gitlab
retval
else
#{send_signature}
super
end
end
EOF
target.prepend(proxy_module)
end
# Small layer of indirection to make it easier to stub out the current
......
......@@ -9,6 +9,7 @@ module Gitlab
return unless current_transaction
current_transaction.increment(:sql_duration, event.duration)
current_transaction.increment(:sql_count, 1)
end
private
......
......@@ -13,13 +13,13 @@ describe AutocompleteController do
project.team << [user2, :developer]
end
let(:body) { JSON.parse(response.body) }
describe 'GET #users with project ID' do
before do
get(:users, project_id: project.id)
end
let(:body) { JSON.parse(response.body) }
it { expect(body).to be_kind_of(Array) }
it { expect(body.size).to eq 2 }
it { expect(body.map { |u| u["username"] }).to include(user.username) }
......@@ -38,6 +38,8 @@ describe AutocompleteController do
get(:users, project_id: project.id, push_code_to_protected_branches: 'true')
end
let(:body) { JSON.parse(response.body) }
it { expect(body).to be_kind_of(Array) }
it { expect(body.size).to eq 1 }
it { expect(body.first["username"]).to eq user.username }
......@@ -154,4 +156,24 @@ describe AutocompleteController do
it { expect(body.size).to eq 0 }
end
end
context 'author of issuable included' do
before do
sign_in(user)
end
let(:body) { JSON.parse(response.body) }
it 'includes the author' do
get(:users, author_id: non_member.id)
expect(body.first["username"]).to eq non_member.username
end
it 'rejects non existent user ids' do
get(:users, author_id: 99999)
expect(body.collect { |u| u['id'] }).not_to include(99999)
end
end
end
......@@ -42,11 +42,9 @@ feature 'issue move to another project' do
expect(current_url).to include project_path(new_project)
page.within('.issue') do
expect(page).to have_content("Text with #{cross_reference}!1")
expect(page).to have_content("Moved from #{cross_reference}#1")
expect(page).to have_content(issue.title)
end
expect(page).to have_content("Text with #{cross_reference}!1")
expect(page).to have_content("Moved from #{cross_reference}#1")
expect(page).to have_content(issue.title)
end
context 'projects user does not have permission to move issue to exist' do
......@@ -74,7 +72,7 @@ feature 'issue move to another project' do
def edit_issue(issue)
visit issue_path(issue)
page.within('.issuable-header') { click_link 'Edit' }
page.within('.issuable-actions') { first(:link, 'Edit').click }
end
def issue_path(issue)
......
......@@ -313,6 +313,23 @@ describe 'Issues', feature: true do
end
end
describe 'new issue' do
context 'dropzone upload file', js: true do
before do
visit new_namespace_project_issue_path(project.namespace, project)
end
it 'should upload file when dragging into textarea' do
drop_in_dropzone test_image_file
# Wait for the file to upload
sleep 1
expect(page.find_field("issue_description").value).to have_content 'banana_sample'
end
end
end
def first_issue
page.all('ul.issues-list > li').first.text
end
......@@ -320,4 +337,25 @@ describe 'Issues', feature: true do
def last_issue
page.all('ul.issues-list > li').last.text
end
def drop_in_dropzone(file_path)
# Generate a fake input selector
page.execute_script <<-JS
var fakeFileInput = window.$('<input/>').attr(
{id: 'fakeFileInput', type: 'file'}
).appendTo('body');
JS
# Attach the file to the fake input selector with Capybara
attach_file("fakeFileInput", file_path)
# Add the file to a fileList array and trigger the fake drop event
page.execute_script <<-JS
var fileList = [$('#fakeFileInput')[0].files[0]];
var e = jQuery.Event('drop', { dataTransfer : { files : fileList } });
$('.div-dropzone')[0].dropzone.listeners[0].events.drop(e);
JS
end
def test_image_file
File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif')
end
end
require 'spec_helper'
feature 'Member autocomplete', feature: true do
let(:project) { create(:project, :public) }
let(:user) { create(:user) }
let(:participant) { create(:user) }
let(:author) { create(:user) }
before do
allow_any_instance_of(Commit).to receive(:author).and_return(author)
login_as user
end
shared_examples "open suggestions" do
it 'suggestions are displayed' do
expect(page).to have_selector('.atwho-view', visible: true)
end
it 'author is suggested' do
page.within('.atwho-view', visible: true) do
expect(page).to have_content(author.username)
end
end
it 'participant is suggested' do
page.within('.atwho-view', visible: true) do
expect(page).to have_content(participant.username)
end
end
end
context 'adding a new note on a Issue', js: true do
before do
issue = create(:issue, author: author, project: project)
create(:note, note: 'Ultralight Beam', noteable: issue, author: participant)
visit_issue(project, issue)
end
context 'when typing @' do
include_examples "open suggestions"
before do
open_member_suggestions
end
end
end
context 'adding a new note on a Merge Request ', js: true do
before do
merge = create(:merge_request, source_project: project, target_project: project, author: author)
create(:note, note: 'Ultralight Beam', noteable: merge, author: participant)
visit_merge_request(project, merge)
end
context 'when typing @' do
include_examples "open suggestions"
before do
open_member_suggestions
end
end
end
context 'adding a new note on a Commit ', js: true do
let(:commit) { project.commit }
before do
allow(commit).to receive(:author).and_return(author)
create(:note_on_commit, author: participant, project: project, commit_id: project.repository.commit.id, note: 'No More Parties in LA')
visit_commit(project, commit)
end
context 'when typing @' do
include_examples "open suggestions"
before do
open_member_suggestions
end
end
end
def open_member_suggestions
sleep 1
page.within('.new-note') do
sleep 1
find('#note_note').native.send_keys('@')
end
end
def visit_issue(project, issue)
visit namespace_project_issue_path(project.namespace, project, issue)
end
def visit_merge_request(project, merge)
visit namespace_project_merge_request_path(project.namespace, project, merge)
end
def visit_commit(project, commit)
visit namespace_project_commit_path(project.namespace, project, commit)
end
end
require 'spec_helper'
feature 'Signup', feature: true do
describe 'signup with no errors' do
it 'creates the user account and sends a confirmation email' do
user = build(:user)
visit root_path
fill_in 'user_name', with: user.name
fill_in 'user_username', with: user.username
fill_in 'user_email', with: user.email
fill_in 'user_password_sign_up', with: user.password
click_button "Sign up"
expect(current_path).to eq user_session_path
expect(page).to have_content("A message with a confirmation link has been sent to your email address.")
end
end
describe 'signup with errors' do
it "displays the errors" do
existing_user = create(:user)
user = build(:user)
visit root_path
fill_in 'user_name', with: user.name
fill_in 'user_username', with: user.username
fill_in 'user_email', with: existing_user.email
fill_in 'user_password_sign_up', with: user.password
click_button "Sign up"
expect(current_path).to eq user_registration_path
expect(page).to have_content("error prohibited this user from being saved")
expect(page).to have_content("Email has already been taken")
end
it 'does not redisplay the password' do
existing_user = create(:user)
user = build(:user)
visit root_path
fill_in 'user_name', with: user.name
fill_in 'user_username', with: user.username
fill_in 'user_email', with: existing_user.email
fill_in 'user_password_sign_up', with: user.password
click_button "Sign up"
expect(current_path).to eq user_registration_path
expect(page.body).not_to match(/#{user.password}/)
end
end
end
......@@ -345,20 +345,76 @@ module Ci
end
end
describe "Variables" do
it "returns variables when defined" do
variables = {
var1: "value1",
var2: "value2",
}
config = YAML.dump({
variables: variables,
before_script: ["pwd"],
rspec: { script: "rspec" }
})
describe 'Variables' do
context 'when global variables are defined' do
it 'returns global variables' do
variables = {
VAR1: 'value1',
VAR2: 'value2',
}
config_processor = GitlabCiYamlProcessor.new(config, path)
expect(config_processor.variables).to eq(variables)
config = YAML.dump({
variables: variables,
before_script: ['pwd'],
rspec: { script: 'rspec' }
})
config_processor = GitlabCiYamlProcessor.new(config, path)
expect(config_processor.global_variables).to eq(variables)
end
end
context 'when job variables are defined' do
context 'when syntax is correct' do
it 'returns job variables' do
variables = {
KEY1: 'value1',
SOME_KEY_2: 'value2'
}
config = YAML.dump(
{ before_script: ['pwd'],
rspec: {
variables: variables,
script: 'rspec' }
})
config_processor = GitlabCiYamlProcessor.new(config, path)
expect(config_processor.job_variables(:rspec)).to eq variables
end
end
context 'when syntax is incorrect' do
it 'raises error' do
variables = [:KEY1, 'value1', :KEY2, 'value2']
config = YAML.dump(
{ before_script: ['pwd'],
rspec: {
variables: variables,
script: 'rspec' }
})
expect { GitlabCiYamlProcessor.new(config, path) }
.to raise_error(GitlabCiYamlProcessor::ValidationError,
/job: variables should be a map/)
end
end
end
context 'when job variables are not defined' do
it 'returns empty array' do
config = YAML.dump({
before_script: ['pwd'],
rspec: { script: 'rspec' }
})
config_processor = GitlabCiYamlProcessor.new(config, path)
expect(config_processor.job_variables(:rspec)).to eq []
end
end
end
......@@ -730,14 +786,14 @@ EOT
config = YAML.dump({ variables: "test", rspec: { script: "test" } })
expect do
GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "variables should be a map of key-valued strings")
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "variables should be a map of key-value strings")
end
it "returns errors if variables is not a map of key-valued strings" do
it "returns errors if variables is not a map of key-value strings" do
config = YAML.dump({ variables: { test: false }, rspec: { script: "test" } })
expect do
GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "variables should be a map of key-valued strings")
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "variables should be a map of key-value strings")
end
it "returns errors if job when is not on_success, on_failure or always" do
......
......@@ -34,9 +34,9 @@ describe Gitlab::BitbucketImport::Importer, lib: true do
let(:project_identifier) { 'namespace/repo' }
let(:data) do
{
bb_session: {
bitbucket_access_token: "123456",
bitbucket_access_token_secret: "secret"
'bb_session' => {
'bitbucket_access_token' => "123456",
'bitbucket_access_token_secret' => "secret"
}
}
end
......@@ -44,7 +44,7 @@ describe Gitlab::BitbucketImport::Importer, lib: true do
create(
:project,
import_source: project_identifier,
import_data: ProjectImportData.new(data: data)
import_data: ProjectImportData.new(credentials: data)
)
end
let(:importer) { Gitlab::BitbucketImport::Importer.new(project) }
......
......@@ -12,7 +12,7 @@ describe Gitlab::GithubImport::ProjectCreator, lib: true do
owner: OpenStruct.new(login: "john")
)
end
let(:namespace){ create(:group, owner: user) }
let(:namespace) { create(:group, owner: user) }
let(:token) { "asdffg" }
let(:access_params) { { github_access_token: token } }
......@@ -27,6 +27,8 @@ describe Gitlab::GithubImport::ProjectCreator, lib: true do
project = project_creator.execute
expect(project.import_url).to eq("https://asdffg@gitlab.com/asd/vim.git")
expect(project.safe_import_url).to eq("https://*****@gitlab.com/asd/vim.git")
expect(project.import_data.credentials).to eq(user: "asdffg", password: nil)
expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
end
end
......@@ -2,11 +2,12 @@ require 'spec_helper'
describe Gitlab::GithubImport::WikiFormatter, lib: true do
let(:project) do
create(:project, namespace: create(:namespace, path: 'gitlabhq'),
import_url: 'https://xxx@github.com/gitlabhq/sample.gitlabhq.git')
create(:project,
namespace: create(:namespace, path: 'gitlabhq'),
import_url: 'https://xxx@github.com/gitlabhq/sample.gitlabhq.git')
end
subject(:wiki) { described_class.new(project)}
subject(:wiki) { described_class.new(project) }
describe '#path_with_namespace' do
it 'appends .wiki to project path' do
......
require 'spec_helper'
describe Gitlab::ImportUrl do
let(:credentials) { { user: 'blah', password: 'password' } }
let(:import_url) do
Gitlab::ImportUrl.new("https://github.com/me/project.git", credentials: credentials)
end
describe :full_url do
it { expect(import_url.full_url).to eq("https://blah:password@github.com/me/project.git") }
end
describe :sanitized_url do
it { expect(import_url.sanitized_url).to eq("https://github.com/me/project.git") }
end
describe :credentials do
it { expect(import_url.credentials).to eq(credentials) }
end
end
......@@ -33,8 +33,16 @@ describe Gitlab::Metrics::Instrumentation do
described_class.instrument_method(@dummy, :foo)
end
it 'renames the original method' do
expect(@dummy).to respond_to(:_original_foo)
it 'instruments the Class' do
target = @dummy.singleton_class
expect(described_class.instrumented?(target)).to eq(true)
end
it 'defines a proxy method' do
mod = described_class.proxy_module(@dummy.singleton_class)
expect(mod.method_defined?(:foo)).to eq(true)
end
it 'calls the instrumented method with the correct arguments' do
......@@ -76,6 +84,14 @@ describe Gitlab::Metrics::Instrumentation do
expect(dummy.method(:test).arity).to eq(0)
end
describe 'when a module is instrumented multiple times' do
it 'calls the instrumented method with the correct arguments' do
described_class.instrument_method(@dummy, :foo)
expect(@dummy.foo).to eq('foo')
end
end
end
describe 'with metrics disabled' do
......@@ -86,7 +102,9 @@ describe Gitlab::Metrics::Instrumentation do
it 'does not instrument the method' do
described_class.instrument_method(@dummy, :foo)
expect(@dummy).to_not respond_to(:_original_foo)
target = @dummy.singleton_class
expect(described_class.instrumented?(target)).to eq(false)
end
end
end
......@@ -100,8 +118,14 @@ describe Gitlab::Metrics::Instrumentation do
instrument_instance_method(@dummy, :bar)
end
it 'renames the original method' do
expect(@dummy.method_defined?(:_original_bar)).to eq(true)
it 'instruments instances of the Class' do
expect(described_class.instrumented?(@dummy)).to eq(true)
end
it 'defines a proxy method' do
mod = described_class.proxy_module(@dummy)
expect(mod.method_defined?(:bar)).to eq(true)
end
it 'calls the instrumented method with the correct arguments' do
......@@ -144,7 +168,7 @@ describe Gitlab::Metrics::Instrumentation do
described_class.
instrument_instance_method(@dummy, :bar)
expect(@dummy.method_defined?(:_original_bar)).to eq(false)
expect(described_class.instrumented?(@dummy)).to eq(false)
end
end
end
......@@ -167,18 +191,17 @@ describe Gitlab::Metrics::Instrumentation do
it 'recursively instruments a class hierarchy' do
described_class.instrument_class_hierarchy(@dummy)
expect(@child1).to respond_to(:_original_child1_foo)
expect(@child2).to respond_to(:_original_child2_foo)
expect(described_class.instrumented?(@child1.singleton_class)).to eq(true)
expect(described_class.instrumented?(@child2.singleton_class)).to eq(true)
expect(@child1.method_defined?(:_original_child1_bar)).to eq(true)
expect(@child2.method_defined?(:_original_child2_bar)).to eq(true)
expect(described_class.instrumented?(@child1)).to eq(true)
expect(described_class.instrumented?(@child2)).to eq(true)
end
it 'does not instrument the root module' do
described_class.instrument_class_hierarchy(@dummy)
expect(@dummy).to_not respond_to(:_original_foo)
expect(@dummy.method_defined?(:_original_bar)).to eq(false)
expect(described_class.instrumented?(@dummy)).to eq(false)
end
end
......@@ -190,7 +213,7 @@ describe Gitlab::Metrics::Instrumentation do
it 'instruments all public class methods' do
described_class.instrument_methods(@dummy)
expect(@dummy).to respond_to(:_original_foo)
expect(described_class.instrumented?(@dummy.singleton_class)).to eq(true)
end
it 'only instruments methods directly defined in the module' do
......@@ -223,7 +246,7 @@ describe Gitlab::Metrics::Instrumentation do
it 'instruments all public instance methods' do
described_class.instrument_instance_methods(@dummy)
expect(@dummy.method_defined?(:_original_bar)).to eq(true)
expect(described_class.instrumented?(@dummy)).to eq(true)
end
it 'only instruments methods directly defined in the module' do
......
......@@ -28,6 +28,9 @@ describe Gitlab::Metrics::Subscribers::ActiveRecord do
expect(transaction).to receive(:increment).
with(:sql_duration, 0.2)
expect(transaction).to receive(:increment).
with(:sql_count, 1)
subscriber.sql(event)
end
end
......
......@@ -238,6 +238,22 @@ describe Ci::Build, models: true do
it { is_expected.to eq(predefined_variables + predefined_trigger_variable + yaml_variables + secure_variables + trigger_variables) }
end
context 'when job variables are defined' do
##
# Job-level variables are defined in gitlab_ci.yml fixture
#
context 'when job variables are unique' do
let(:build) { create(:ci_build, name: 'staging') }
it 'includes job variables' do
expect(subject).to include(
{ key: :KEY1, value: 'value1', public: true },
{ key: :KEY2, value: 'value2', public: true }
)
end
end
end
end
end
end
......
......@@ -36,4 +36,19 @@ describe ExternalIssue, models: true do
expect(issue.title).to eq "External Issue #{issue}"
end
end
describe '#reference_link_text' do
context 'if issue id has a prefix' do
it 'returns the issue ID' do
expect(issue.reference_link_text).to eq 'EXT-1234'
end
end
context 'if issue id is a number' do
let(:issue) { described_class.new('1234', project) }
it 'returns the issue ID prefixed by #' do
expect(issue.reference_link_text).to eq '#1234'
end
end
end
end
......@@ -816,11 +816,9 @@ describe Repository, models: true do
describe '#rm_tag' do
it 'removes a tag' do
expect(repository).to receive(:before_remove_tag)
expect(repository.rugged.tags).to receive(:delete).with('v1.1.0')
expect_any_instance_of(Gitlab::Shell).to receive(:rm_tag).
with(repository.path_with_namespace, '8.5')
repository.rm_tag('8.5')
repository.rm_tag('v1.1.0')
end
end
......@@ -1022,6 +1020,30 @@ describe Repository, models: true do
end
end
describe '.clean_old_archives' do
let(:path) { Gitlab.config.gitlab.repository_downloads_path }
context 'when the downloads directory does not exist' do
it 'does not remove any archives' do
expect(File).to receive(:directory?).with(path).and_return(false)
expect(Gitlab::Popen).not_to receive(:popen)
described_class.clean_old_archives
end
end
context 'when the downloads directory exists' do
it 'removes old archives' do
expect(File).to receive(:directory?).with(path).and_return(true)
expect(Gitlab::Popen).to receive(:popen)
described_class.clean_old_archives
end
end
end
def create_remote_branch(remote_name, branch_name, target)
rugged = repository.rugged
rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", target)
......
......@@ -6,21 +6,12 @@ describe DeleteTagService, services: true do
let(:user) { create(:user) }
let(:service) { described_class.new(project, user) }
let(:tag) { double(:tag, name: '8.5', target: 'abc123') }
describe '#execute' do
before do
allow(repository).to receive(:find_tag).and_return(tag)
end
it 'removes the tag' do
expect_any_instance_of(Gitlab::Shell).to receive(:rm_tag).
and_return(true)
expect(repository).to receive(:before_remove_tag)
expect(service).to receive(:success)
service.execute('8.5')
service.execute('v1.1.0')
end
end
end
......@@ -4,7 +4,7 @@ services:
before_script:
- gem install bundler
- bundle install
- bundle install
- bundle exec rake db:create
variables:
......@@ -17,7 +17,7 @@ types:
rspec:
script: "rake spec"
tags:
tags:
- ruby
- postgres
only:
......@@ -26,27 +26,32 @@ rspec:
spinach:
script: "rake spinach"
allow_failure: true
tags:
tags:
- ruby
- mysql
except:
- tags
staging:
variables:
KEY1: value1
KEY2: value2
script: "cap deploy stating"
type: deploy
tags:
tags:
- ruby
- mysql
except:
- stable
production:
variables:
DB_NAME: mysql
type: deploy
script:
script:
- cap deploy production
- cap notify
tags:
tags:
- ruby
- mysql
only:
......
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