Commit b2d45bb5 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge branch 'master' of dev.gitlab.org:gitlab/gitlabhq into ce-to-ee

Conflicts:
	app/controllers/uploads_controller.rb
	app/helpers/application_helper.rb
	app/models/note.rb
	app/services/test_hook_service.rb
	db/schema.rb
	spec/models/note_spec.rb
parents e9af4c71 cb8f974b
Please view this file on the master branch, on stable branches it's out of date.
v 7.11.0 (unreleased)
- Don't show duplicate deploy keys
- Fix commit time being displayed in the wrong timezone in some cases (Hannes Rosenögger)
- Make the first branch pushed to an empty repository the default HEAD (Stan Hu)
- Fix broken view when using a tag to display a tree that contains git submodules (Stan Hu)
- Make Reply-To config apply to change e-mail confirmation and other Devise notifications (Stan Hu)
- Add application setting to restrict user signups to e-mail domains (Stan Hu)
- Don't allow a merge request to be merged when its title starts with "WIP".
......@@ -20,7 +24,10 @@ v 7.11.0 (unreleased)
- Add "Reply quoting selected text" shortcut key (`r`)
- Fix bug causing `@whatever` inside an issue's first code block to be picked up as a user mention.
- Fix bug causing `@whatever` inside an inline code snippet (backtick-style) to be picked up as a user mention.
-
- When use change branches link at MR form - save source branch selection instead of target one
- Improve handling of large diffs
- Added GitLab Event header for project hooks
- Add Two-factor authentication (2FA) for GitLab logins
- Show Atom feed buttons everywhere where applicable.
- Add project activity atom feed.
- Don't crash when an MR from a fork has a cross-reference comment from the target project on one of its commits.
......@@ -32,7 +39,8 @@ v 7.11.0 (unreleased)
- Show incompatible projects in Google Code import status (Stan Hu)
- Fix bug where commit data would not appear in some subdirectories (Stan Hu)
- Unescape branch names in compare commit (Stan Hu)
-
- Task lists are now usable in comments, and will show up in Markdown previews.
- Fix bug where avatar filenames were not actually deleted from the database during removal (Stan Hu)
- Fix bug where Slack service channel was not saved in admin template settings. (Stan Hu)
- Move snippets UI to fluid layout
- Improve UI for sidebar. Increase separation between navigation and content
......@@ -44,6 +52,10 @@ v 7.11.0 (unreleased)
- Allow to use non-ASCII letters and dashes in project and namespace name. (Jakub Jirutka)
- Add footnotes support to Markdown (Guillaume Delbergue)
- Add current_sign_in_at to UserFull REST api.
- Make Sidekiq MemoryKiller shutdown signal configurable
- Add "Create Merge Request" buttons to commits and branches pages and push event.
- Show user roles by comments.
- Fix automatic blocking of auto-created users from Active Directory.
v 7.10.2
- Fix CI links on MR page
......
......@@ -63,7 +63,7 @@ Merge requests can be filed either at [gitlab.com](https://gitlab.com/gitlab-org
If you are new to GitLab development (or web development in general), search for the label `easyfix` ([gitlab.com](https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=easyfix), [github](https://github.com/gitlabhq/gitlabhq/labels/easyfix)). Those are issues easy to fix, marked by the GitLab core-team. If you are unsure how to proceed but want to help, mention one of the core-team members to give you a hint.
To start with GitLab download the [GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit) and see [Development section](doc/development/README.md) in the help file.
To start with GitLab download the [GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit) and see [Development section](doc/development/README.md) in the help file.
### Merge request guidelines
......@@ -161,6 +161,7 @@ If you add a dependency in GitLab (such as an operating system package) please c
1. [Shell commands](doc/development/shell_commands.md) created by GitLab contributors to enhance security
1. [Markdown](http://www.cirosantilli.com/markdown-styleguide)
1. Interface text should be written subjectively instead of objectively. It should be the gitlab core team addressing a person. It should be written in present time and never use past tense (has been/was). For example instead of "prohibited this user from being saved due to the following errors:" the text should be "sorry, we could not create your account because:". Also these [excellent writing guidelines](https://github.com/NARKOZ/guides#writing).
1. [Migrations](doc/development/migration_style_guide.md)
This is also the style used by linting tools such as [RuboCop](https://github.com/bbatsov/rubocop), [PullReview](https://www.pullreview.com/) and [Hound CI](https://houndci.com).
......
......@@ -28,18 +28,23 @@ gem 'omniauth-google-oauth2'
gem 'omniauth-twitter'
gem 'omniauth-github'
gem 'omniauth-shibboleth'
gem 'omniauth-kerberos'
gem 'omniauth-kerberos', group: :kerberos
gem 'omniauth-gitlab'
gem 'omniauth-bitbucket'
gem 'doorkeeper', '2.1.3'
gem "rack-oauth2", "~> 1.0.5"
# Two-factor authentication
gem 'devise-two-factor'
gem 'rqrcode-rails3'
gem 'attr_encrypted', '1.3.4'
# Browser detection
gem "browser"
# Extracting information from a git repository
# Provide access to Gitlab::Git library
gem "gitlab_git", '~> 7.1.10'
gem "gitlab_git", '~> 7.1.11'
# Ruby/Rack Git Smart-HTTP Server Handler
gem 'gitlab-grack', '~> 2.0.2', require: 'grack'
......@@ -88,20 +93,17 @@ gem "six"
# Seed data
gem "seed-fu"
# Markup pipeline for GitLab
# Markdown and HTML processing
gem 'html-pipeline', '~> 1.11.0'
# Markdown to HTML
gem "github-markup"
# Required markup gems by github-markdown
gem 'redcarpet', '~> 3.2.3'
gem 'task_list', '~> 1.0.0', require: 'task_list/railtie'
gem 'github-markup'
gem 'redcarpet', '~> 3.2.3'
gem 'RedCloth'
gem 'rdoc', '~>3.6'
gem 'org-ruby', '= 0.9.12'
gem 'creole', '~>0.3.6'
gem 'wikicloth', '=0.8.1'
gem 'asciidoctor', '= 0.1.4'
gem 'rdoc', '~>3.6'
gem 'org-ruby', '= 0.9.12'
gem 'creole', '~>0.3.6'
gem 'wikicloth', '=0.8.1'
gem 'asciidoctor', '= 0.1.4'
# Diffs
gem 'diffy', '~> 3.0.3'
......@@ -254,7 +256,6 @@ group :development, :test do
# PhantomJS driver for Capybara
gem 'poltergeist', '~> 1.5.1'
gem 'jasmine', '~> 2.2.0'
gem 'jasmine-rails'
gem "spring", '~> 1.3.1'
......
......@@ -46,6 +46,8 @@ GEM
ast (2.0.0)
astrolabe (1.3.0)
parser (>= 2.2.0.pre.3, < 3.0)
attr_encrypted (1.3.4)
encryptor (>= 1.3.0)
attr_required (1.0.0)
autoprefixer-rails (5.1.11)
execjs
......@@ -136,6 +138,13 @@ GEM
warden (~> 1.2.3)
devise-async (0.9.0)
devise (~> 3.2)
devise-two-factor (1.0.1)
activemodel
activesupport
attr_encrypted (~> 1.3.2)
devise (~> 3.2.4)
rails
rotp (~> 1.6.1)
diff-lcs (1.2.5)
diffy (3.0.3)
docile (1.1.5)
......@@ -147,6 +156,7 @@ GEM
email_spec (1.5.0)
launchy (~> 2.1)
mail (~> 2.2)
encryptor (1.3.0)
enumerize (0.7.0)
activesupport (>= 3.2)
equalizer (0.0.8)
......@@ -216,7 +226,7 @@ GEM
mime-types (~> 1.19)
gitlab_emoji (0.1.0)
gemojione (~> 2.0)
gitlab_git (7.1.10)
gitlab_git (7.1.11)
activesupport (~> 4.0)
charlock_holmes (~> 0.6)
gitlab-linguist (~> 3.0)
......@@ -291,11 +301,6 @@ GEM
i18n (0.7.0)
ice_cube (0.11.1)
ice_nine (0.10.0)
jasmine (2.2.0)
jasmine-core (~> 2.2)
phantomjs
rack (>= 1.2.1)
rake
jasmine-core (2.2.0)
jasmine-rails (0.10.8)
jasmine-core (>= 1.3, < 3.0)
......@@ -488,7 +493,11 @@ GEM
rest-client (1.6.7)
mime-types (>= 1.16)
rinku (1.7.3)
rotp (1.6.1)
rouge (1.7.7)
rqrcode (0.4.2)
rqrcode-rails3 (0.1.7)
rqrcode (>= 0.4.2)
rspec (2.99.0)
rspec-core (~> 2.99.0)
rspec-expectations (~> 2.99.0)
......@@ -598,6 +607,8 @@ GEM
stamp (0.5.0)
state_machine (1.2.0)
stringex (2.5.2)
task_list (1.0.2)
html-pipeline
temple (0.6.7)
term-ansicolor (1.2.2)
tins (~> 0.8)
......@@ -674,6 +685,7 @@ DEPENDENCIES
annotate (~> 2.6.0.beta2)
asana (~> 0.0.6)
asciidoctor (= 0.1.4)
attr_encrypted (= 1.3.4)
awesome_print
better_errors
binding_of_caller
......@@ -695,6 +707,7 @@ DEPENDENCIES
default_value_for (~> 3.0.0)
devise (= 3.2.4)
devise-async (= 0.9.0)
devise-two-factor
diffy (~> 3.0.3)
doorkeeper (= 2.1.3)
dropzonejs-rails
......@@ -712,7 +725,7 @@ DEPENDENCIES
gitlab-license (~> 0.0.2)
gitlab-linguist (~> 3.0.1)
gitlab_emoji (~> 0.1)
gitlab_git (~> 7.1.10)
gitlab_git (~> 7.1.11)
gitlab_meta (= 7.0)
gitlab_omniauth-ldap (= 1.2.1)
gollum-lib (~> 4.0.2)
......@@ -726,7 +739,6 @@ DEPENDENCIES
hipchat (~> 1.5.0)
html-pipeline (~> 1.11.0)
httparty
jasmine (~> 2.2.0)
jasmine-rails
jquery-atwho-rails (~> 1.0.0)
jquery-rails
......@@ -769,6 +781,7 @@ DEPENDENCIES
redis-rails
request_store
rerun (~> 0.10.0)
rqrcode-rails3
rspec-rails (= 2.99)
rubocop (= 0.28.0)
rugments
......@@ -792,6 +805,7 @@ DEPENDENCIES
spring-commands-spinach (= 1.0.0)
stamp
state_machine
task_list (~> 1.0.0)
test_after_commit
thin
tinder (~> 1.9.2)
......
......@@ -32,7 +32,7 @@ For all other questions, contact us at sales@gitlab.com
## Open source software to collaborate on code
![Animated screenshots](https://about.gitlab.com/images/animated/compiled.gif)
To see how GitLab looks please see the [features page on our website](https://about.gitlab.com/features/).
- Manage Git repositories with fine grained access controls that keep your code secure
- Perform code reviews and enhance collaboration with merge requests
......@@ -127,4 +127,4 @@ Please see [Getting help for GitLab](https://about.gitlab.com/getting-help/) on
## Is it awesome?
Thanks for [asking this question](https://twitter.com/supersloth/status/489462789384056832) Joshua.
[These people](https://twitter.com/gitlab/favorites) seem to like it.
[These people](https://twitter.com/gitlab/favorites) seem to like it.
\ No newline at end of file
window.updateTaskState = (taskableType) ->
objType = taskableType.data
isChecked = $(this).prop("checked")
if $(this).is(":checked")
stateEvent = "task_check"
else
stateEvent = "task_uncheck"
taskableUrl = $("form.edit-" + objType).first().attr("action")
taskableNum = taskableUrl.match(/\d+$/)
taskNum = 0
$("li.task-list-item input:checkbox").each( (index, e) =>
if e == this
taskNum = index + 1
)
$.ajax
type: "PATCH"
url: taskableUrl
data: objType + "[state_event]=" + stateEvent +
"&" + objType + "[task_num]=" + taskNum
......@@ -116,6 +116,13 @@ class Dispatcher
new NamespaceSelect()
when 'dashboard'
shortcut_handler = new ShortcutsDashboardNavigation()
switch path[1]
when 'issues', 'merge_requests'
new UsersSelect()
when 'groups'
switch path[1]
when 'issues', 'merge_requests'
new UsersSelect()
when 'profiles'
new Profile()
when 'projects'
......
#= require jquery
#= require jquery.waitforimages
#= require task_list
class @Issue
constructor: ->
$('.edit-issue.inline-update input[type="submit"]').hide()
......@@ -6,11 +10,11 @@ class @Issue
$(".context .inline-update").on "change", "#issue_assignee_id", ->
$(this).submit()
if $("a.btn-close").length
$("li.task-list-item input:checkbox").prop("disabled", false)
# Prevent duplicate event bindings
@disableTaskList()
$('.task-list-item input:checkbox').off('change')
$('.task-list-item input:checkbox').change('issue', updateTaskState)
if $("a.btn-close").length
@initTaskList()
$('.issue-details').waitForImages ->
$('.issuable-affix').affix offset:
......@@ -22,3 +26,22 @@ class @Issue
$(@).width($(@).outerWidth())
.on 'affixed-top.bs.affix affixed-bottom.bs.affix', ->
$(@).width('')
initTaskList: ->
$('.issue-details .js-task-list-container').taskList('enable')
$(document).on 'tasklist:changed', '.issue-details .js-task-list-container', @updateTaskList
disableTaskList: ->
$('.issue-details .js-task-list-container').taskList('disable')
$(document).off 'tasklist:changed', '.issue-details .js-task-list-container'
# TODO (rspeicher): Make the issue description inline-editable like a note so
# that we can re-use its form here
updateTaskList: ->
patchData = {}
patchData['issue'] = {'description': $('.js-task-list-field', this).val()}
$.ajax
type: 'PATCH'
url: $('form.js-issue-update').attr('action')
data: patchData
#= require jquery
#= require bootstrap
#= require task_list
class @MergeRequest
constructor: (@opts) ->
@initContextWidget()
......@@ -17,8 +21,11 @@ class @MergeRequest
disableButtonIfEmptyField '#commit_message', '.accept_merge_request'
# Prevent duplicate event bindings
@disableTaskList()
if $("a.btn-close").length
$("li.task-list-item input:checkbox").prop("disabled", false)
@initTaskList()
$('.merge-request-details').waitForImages ->
$('.issuable-affix').affix offset:
......@@ -77,9 +84,6 @@ class @MergeRequest
this.$('.remove_source_branch_in_progress').hide()
this.$('.remove_source_branch_widget.failed').show()
$('.task-list-item input:checkbox').off('change')
$('.task-list-item input:checkbox').change('merge_request', updateTaskState)
activateTab: (action) ->
this.$('.merge-request-tabs li').removeClass 'active'
this.$('.tab-content').hide()
......@@ -156,3 +160,22 @@ class @MergeRequest
else
setTimeout(merge_request.mergeInProgress, 3000)
dataType: 'json'
initTaskList: ->
$('.merge-request-details .js-task-list-container').taskList('enable')
$(document).on 'tasklist:changed', '.merge-request-details .js-task-list-container', @updateTaskList
disableTaskList: ->
$('.merge-request-details .js-task-list-container').taskList('disable')
$(document).off 'tasklist:changed', '.merge-request-details .js-task-list-container'
# TODO (rspeicher): Make the merge request description inline-editable like a
# note so that we can re-use its form here
updateTaskList: ->
patchData = {}
patchData['merge_request'] = {'description': $('.js-task-list-field', this).val()}
$.ajax
type: 'PATCH'
url: $('form.js-merge-request-update').attr('action')
data: patchData
#= require jquery
#= require autosave
#= require bootstrap
#= require dropzone
#= require dropzone_input
#= require gfm_auto_complete
#= require jquery.atwho
#= require task_list
class @Notes
@interval: null
......@@ -11,6 +20,7 @@ class @Notes
@setupMainTargetNoteForm()
@cleanBinding()
@addBinding()
@initTaskList()
addBinding: ->
# add note to UI after creation
......@@ -81,6 +91,9 @@ class @Notes
$(document).off "click", ".js-note-target-reopen"
$(document).off "click", ".js-note-target-close"
$('.note .js-task-list-container').taskList('disable')
$(document).off 'tasklist:changed', '.note .js-task-list-container'
initRefresh: ->
clearInterval(Notes.interval)
Notes.interval = setInterval =>
......@@ -114,6 +127,7 @@ class @Notes
if @isNewNote(note)
@note_ids.push(note.id)
$('ul.main-notes-list').append(note.html)
@initTaskList()
###
Check if note does not exists on page
......@@ -268,6 +282,8 @@ class @Notes
note_li.replaceWith(note.html)
note_li.find('.note-edit-form').hide()
note_li.find('.note-body > .note-text').show()
note_li.find('js-task-list-container').taskList('enable')
@enableTaskList()
###
Called in response to clicking the edit note link
......@@ -479,3 +495,13 @@ class @Notes
else
form.find('.js-note-target-reopen').text('Reopen')
form.find('.js-note-target-close').text('Close')
initTaskList: ->
@enableTaskList()
$(document).on 'tasklist:changed', '.note .js-task-list-container', @updateTaskList
enableTaskList: ->
$('.note .js-task-list-container').taskList('enable')
updateTaskList: ->
$('form', this).submit()
......@@ -21,6 +21,10 @@
@include border-radius($radius 0 0 $radius)
}
@mixin border-radius-right($radius) {
@include border-radius(0 0 $radius $radius)
}
@mixin linear-gradient($from, $to) {
background-image: -webkit-gradient(linear, 0 0, 0 100%, from($from), to($to));
background-image: -webkit-linear-gradient($from, $to);
......
......@@ -15,7 +15,7 @@ header {
padding: 0;
background: #FFF;
border-bottom: 1px solid #DDD;
border-bottom: 1px solid #EEE;
filter: none;
.title {
......@@ -25,13 +25,13 @@ header {
margin-left: 25px;
font-size: 18px;
line-height: 44px;
font-weight: normal;
color: #555;
font-weight: bold;
color: #444;
@include str-truncated(37%);
a {
color: #555;
color: #444;
&:hover {
text-decoration: underline;
}
......
......@@ -37,7 +37,9 @@ pre {
position: relative;
a.anchor {
display: none;
// Setting `display: none` would prevent the anchor being scrolled to, so
// instead we set the height to 0 and it gets updated on hover.
height: 0;
}
&:hover > a.anchor {
......
......@@ -29,10 +29,6 @@
.commits-feed-holder {
float: right;
.btn {
padding: 4px 12px;
}
}
li.commit {
......
......@@ -44,6 +44,14 @@ ul.notes {
}
.author-username {
}
.note-role {
float: right;
margin-top: 2px;
border: 1px solid #bbb;
background-color: transparent;
color: #999;
}
}
.discussion {
......@@ -62,6 +70,16 @@ ul.notes {
word-wrap: break-word;
@include md-typography;
// Reduce left padding of first ul element
ul.task-list:first-child {
padding-left: 10px;
// sub-lists should be padded normally
ul {
padding-left: 20px;
}
}
hr {
margin: 10px 0;
}
......@@ -126,6 +144,7 @@ ul.notes {
.note {
&.note:hover {
.note-actions { display: block; }
.note-actions + .note-role { display: none; }
}
.discussion-header:hover {
.discussion-actions { display: block; }
......@@ -143,6 +162,8 @@ ul.notes {
}
a {
margin-left: 5px;
@extend .cgray;
&:hover {
......
......@@ -102,6 +102,15 @@
.input-group-addon {
background: #FAFAFA;
&.git-protocols {
padding: 0;
border: none;
.input-group-btn:last-child > .btn {
@include border-radius-right(0);
}
}
}
}
......@@ -201,9 +210,12 @@ ul.nav.nav-projects-tabs {
}
.well {
padding: 14px;
h4 {
font-weight: normal;
margin: 0;
color: #555;
}
.nav-pills a {
......@@ -237,7 +249,8 @@ ul.nav.nav-projects-tabs {
}
.breadcrumb.repo-breadcrumb {
padding: 2px 0;
padding: 0;
line-height: 34px;
background: white;
border: none;
font-size: 16px;
......
......@@ -106,17 +106,9 @@
}
}
.tree-download-holder .btn {
padding: 4px 12px;
}
.tree-ref-holder {
float: left;
margin-right: 15px;
.select2-container .select2-choice, .select2-container.select2-drop-above .select2-choice {
padding: 4px 12px;
}
}
.readme-holder {
......
......@@ -33,7 +33,7 @@ class Admin::HooksController < Admin::ApplicationController
owner_name: "Someone",
owner_email: "example@gitlabhq.com"
}
@hook.execute(data)
@hook.execute(data, 'system_hooks')
redirect_to :back
end
......
......@@ -252,7 +252,7 @@ class ApplicationController < ActionController::Base
end
def configure_permitted_parameters
devise_parameter_sanitizer.sanitize(:sign_in) { |u| u.permit(:username, :email, :password, :login, :remember_me) }
devise_parameter_sanitizer.for(:sign_in) { |u| u.permit(:username, :email, :password, :login, :remember_me, :otp_attempt) }
end
def hexdigest(string)
......
......@@ -15,4 +15,25 @@ class PasswordsController < Devise::PasswordsController
respond_with(resource)
end
end
# After a user resets their password, prompt for 2FA code if enabled instead
# of signing in automatically
#
# See http://git.io/vURrI
def update
super do |resource|
# TODO (rspeicher): In Devise master (> 3.4.1), we can set
# `Devise.sign_in_after_reset_password = false` and avoid this mess.
if resource.errors.empty? && resource.try(:otp_required_for_login?)
resource.unlock_access! if unlockable?(resource)
# Since we are not signing this user in, we use the :updated_not_active
# message which only contains "Your password was changed successfully."
set_flash_message(:notice, :updated_not_active) if is_flashing_format?
# Redirect to sign in so they can enter 2FA code
respond_with(resource, location: new_session_path(resource)) and return
end
end
end
end
class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
def new
unless current_user.otp_secret
current_user.otp_secret = User.generate_otp_secret
current_user.save!
end
@qr_code = build_qr_code
end
def create
if current_user.valid_otp?(params[:pin_code])
current_user.otp_required_for_login = true
@codes = current_user.generate_otp_backup_codes!
current_user.save!
render 'create'
else
@error = 'Invalid pin code'
@qr_code = build_qr_code
render 'new'
end
end
def codes
@codes = current_user.generate_otp_backup_codes!
current_user.save!
end
def destroy
current_user.update_attributes({
otp_required_for_login: false,
encrypted_otp_secret: nil,
encrypted_otp_secret_iv: nil,
encrypted_otp_secret_salt: nil,
otp_backup_codes: nil
})
redirect_to profile_account_path
end
private
def build_qr_code
issuer = "GitLab | #{current_user.email}"
uri = current_user.otp_provisioning_uri(current_user.email, issuer: issuer)
RQRCode::render_qrcode(uri, :svg, level: :m, unit: 3)
end
end
......@@ -6,11 +6,12 @@ class Projects::CompareController < Projects::ApplicationController
before_action :authorize_download_code!
def index
@ref = Addressable::URI.unescape(params[:to])
end
def show
base_ref = Addressable::URI.unescape(params[:from])
head_ref = Addressable::URI.unescape(params[:to])
@ref = head_ref = Addressable::URI.unescape(params[:to])
compare_result = CompareService.new.execute(
current_user,
......
class SessionsController < Devise::SessionsController
prepend_before_action :authenticate_with_two_factor, only: [:create]
# This action comes from DeviseController, but because we call `sign_in`
# manually inside `authenticate_with_two_factor`, not skipping this action
# would cause a "You are already signed in." error message to be shown upon
# successful login.
skip_before_action :require_no_authentication, only: [:create]
def new
redirect_path =
if request.referer.present? && (params['redirect_to_referer'] == 'yes')
......@@ -14,7 +22,7 @@ class SessionsController < Devise::SessionsController
# Prevent a 'you are already signed in' message directly after signing:
# we should never redirect to '/users/sign_in' after signing in successfully.
unless redirect_path == '/users/sign_in'
unless redirect_path == new_user_session_path
store_location_for(:redirect, redirect_path)
end
......@@ -27,11 +35,54 @@ class SessionsController < Devise::SessionsController
def create
super do |resource|
# User has successfully signed in, so clear any unused reset tokens
# User has successfully signed in, so clear any unused reset token
if resource.reset_password_token.present?
resource.update_attributes(reset_password_token: nil,
reset_password_sent_at: nil)
end
end
end
private
def user_params
params.require(:user).permit(:login, :password, :remember_me, :otp_attempt)
end
def find_user
if user_params[:login]
User.by_login(user_params[:login])
elsif user_params[:otp_attempt] && session[:otp_user_id]
User.find(session[:otp_user_id])
end
end
def authenticate_with_two_factor
user = self.resource = find_user
return unless user && user.otp_required_for_login
if user_params[:otp_attempt].present? && session[:otp_user_id]
if valid_otp_attempt?(user)
# Remove any lingering user data from login
session.delete(:otp_user_id)
sign_in(user) and return
else
flash.now[:alert] = 'Invalid two-factor code.'
render :two_factor and return
end
else
if user && user.valid_password?(user_params[:password])
# Save the user's ID to session so we can ask for a one-time password
session[:otp_user_id] = user.id
render :two_factor and return
end
end
end
def valid_otp_attempt?(user)
user.valid_otp?(user_params[:otp_attempt]) ||
user.invalidate_otp_backup_code!(user_params[:otp_attempt])
end
end
......@@ -52,14 +52,14 @@ class UploadsController < ApplicationController
def upload_model
upload_models = {
user: User,
project: Project,
note: Note,
group: Group,
appearance: Appearance
"user" => User,
"project" => Project,
"note" => Note,
"group" => Group,
"appearance" => Appearance
}
upload_models[params[:model].to_sym]
upload_models[params[:model]]
end
def upload_mount
......
......@@ -219,7 +219,7 @@ module ApplicationHelper
data: { toggle: 'tooltip', placement: placement }
else
haml_tag :time, date.to_s,
class: html_class, datetime: date.getutc.iso8601, title: date.stamp('Aug 21, 2011 9:23pm'),
class: html_class, datetime: date.getutc.iso8601, title: date.in_time_zone.stamp('Aug 21, 2011 9:23pm'),
data: { toggle: 'tooltip', placement: placement }
end
......
module CompareHelper
def compare_to_mr_button?
@project.merge_requests_enabled &&
params[:from].present? &&
params[:to].present? &&
@repository.branch_names.include?(params[:from]) &&
@repository.branch_names.include?(params[:to]) &&
params[:from] != params[:to] &&
!@refs_are_same
def create_mr_button?(from = params[:from], to = params[:to], project = @project)
from.present? &&
to.present? &&
from != to &&
project.merge_requests_enabled &&
project.repository.branch_names.include?(from) &&
project.repository.branch_names.include?(to)
end
def compare_mr_path
def create_mr_path(from = params[:from], to = params[:to], project = @project)
new_namespace_project_merge_request_path(
@project.namespace,
@project,
project.namespace,
project,
merge_request: {
source_branch: params[:to],
target_branch: params[:from]
source_branch: to,
target_branch: from
}
)
end
......
......@@ -7,14 +7,23 @@ module DiffHelper
end
end
def safe_diff_files(diffs)
diffs.first(allowed_diff_size).map do |diff|
Gitlab::Diff::File.new(diff)
def allowed_diff_lines
if diff_hard_limit_enabled?
Commit::DIFF_HARD_LIMIT_LINES
else
Commit::DIFF_SAFE_LINES
end
end
def show_diff_size_warning?(diffs)
diffs.size > allowed_diff_size
def safe_diff_files(diffs)
lines = 0
safe_files = []
diffs.first(allowed_diff_size).each do |diff|
lines += diff.diff.lines.count
break if lines > allowed_diff_lines
safe_files << Gitlab::Diff::File.new(diff)
end
safe_files
end
def diff_hard_limit_enabled?
......
......@@ -25,12 +25,16 @@ module EventsHelper
def event_filter_link(key, tooltip)
key = key.to_s
active = if @event_filter.active? key
'active'
end
active = 'active' if @event_filter.active?(key)
link_opts = {
class: 'event_filter_link',
id: "#{key}_event_filter",
title: "Filter by #{tooltip.downcase}",
data: { toggle: 'tooltip', placement: 'top' }
}
content_tag :li, class: "filter_icon #{active}" do
link_to request.path, class: 'has_tooltip event_filter_link', id: "#{key}_event_filter", 'data-original-title' => 'Filter by ' + tooltip.downcase do
link_to request.path, link_opts do
icon(icon_for_event[key]) + content_tag(:span, ' ' + tooltip)
end
end
......
......@@ -26,15 +26,15 @@ module IconsHelper
end
def public_icon
icon('globe')
icon('globe fw')
end
def internal_icon
icon('shield')
icon('shield fw')
end
def private_icon
icon('lock')
icon('lock fw')
end
def file_type_icon_class(type, mode, name)
......
......@@ -56,4 +56,16 @@ module MergeRequestsHelper
def issues_sentence(issues)
issues.map { |i| "##{i.iid}" }.to_sentence
end
def mr_change_branches_path(merge_request)
new_namespace_project_merge_request_path(
@project.namespace, @project,
merge_request: {
source_project_id: @merge_request.source_project_id,
target_project_id: @merge_request.target_project_id,
source_branch: @merge_request.source_branch,
target_branch: nil
}
)
end
end
......@@ -9,6 +9,10 @@ module NotesHelper
hidden_field_tag(:target_id, note.noteable.id)
end
def note_editable?(note)
note.editable? && can?(current_user, :admin_note, note)
end
def link_to_commit_diff_line_note(note)
if note.for_commit_diff_line?
link_to(
......
......@@ -39,7 +39,7 @@ module Mentionable
# Determine whether or not a cross-reference Note has already been created between this Mentionable and
# the specified target.
def has_mentioned?(target)
Note.cross_reference_exists?(target, local_reference)
SystemNoteService.cross_reference_exists?(target, local_reference)
end
def mentioned_users(current_user = nil, p = project)
......
require 'task_list'
# Contains functionality for objects that can have task lists in their
# descriptions. Task list items can be added with Markdown like "* [x] Fix
# bugs".
#
# Used by MergeRequest and Issue
module Taskable
TASK_PATTERN_MD = /^(?<bullet> *[*-] *)\[(?<checked>[ xX])\]/.freeze
TASK_PATTERN_HTML = /^<li>(?<p_tag>\s*<p>)?\[(?<checked>[ xX])\]/.freeze
# Change the state of a task list item for this Taskable. Edit the object's
# description by finding the nth task item and changing its checkbox
# placeholder to "[x]" if +checked+ is true, or "[ ]" if it's false.
# Note: task numbering starts with 1
def update_nth_task(n, checked)
index = 0
check_char = checked ? 'x' : ' '
# Called by `TaskList::Summary`
def task_list_items
return [] if description.blank?
# Do this instead of using #gsub! so that ActiveRecord detects that a field
# has changed.
self.description = self.description.gsub(TASK_PATTERN_MD) do |match|
index += 1
case index
when n then "#{$LAST_MATCH_INFO[:bullet]}[#{check_char}]"
else match
end
@task_list_items ||= description.scan(TaskList::Filter::ItemPattern).collect do |item|
# ItemPattern strips out the hyphen, but Item requires it. Rabble rabble.
TaskList::Item.new("- #{item}")
end
end
save
def tasks
@tasks ||= TaskList.new(self)
end
# Return true if this object's description has any task list items.
def tasks?
description && description.match(TASK_PATTERN_MD)
tasks.summary.items?
end
# Return a string that describes the current state of this Taskable's task
# list items, e.g. "20 tasks (12 done, 8 unfinished)"
# list items, e.g. "20 tasks (12 completed, 8 remaining)"
def task_status
return nil unless description
num_tasks = 0
num_done = 0
description.scan(TASK_PATTERN_MD) do
num_tasks += 1
num_done += 1 unless $LAST_MATCH_INFO[:checked] == ' '
end
return '' if description.blank?
"#{num_tasks} tasks (#{num_done} done, #{num_tasks - num_done} unfinished)"
sum = tasks.summary
"#{sum.item_count} tasks (#{sum.complete_count} completed, #{sum.incomplete_count} remaining)"
end
end
......@@ -24,7 +24,7 @@ class Group < Namespace
has_many :ldap_group_links, foreign_key: 'group_id', dependent: :destroy
has_many :hooks, dependent: :destroy, class_name: 'GroupHook'
validate :avatar_type, if: ->(user) { user.avatar_changed? }
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
mount_uploader :avatar, AvatarUploader
......
......@@ -17,4 +17,8 @@
class ServiceHook < WebHook
belongs_to :service
def execute(data)
super(data, 'service_hook')
end
end
......@@ -30,12 +30,15 @@ class WebHook < ActiveRecord::Base
validates :url, presence: true,
format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" }
def execute(data)
def execute(data, hook_name)
parsed_url = URI.parse(url)
if parsed_url.userinfo.blank?
WebHook.post(url,
body: data.to_json,
headers: { "Content-Type" => "application/json" },
headers: {
"Content-Type" => "application/json",
"X-Gitlab-Event" => hook_name.singularize.titleize
},
verify: false)
else
post_url = url.gsub("#{parsed_url.userinfo}@", "")
......@@ -45,7 +48,10 @@ class WebHook < ActiveRecord::Base
}
WebHook.post(post_url,
body: data.to_json,
headers: { "Content-Type" => "application/json" },
headers: {
"Content-Type" => "application/json",
"X-Gitlab-Event" => hook_name.singularize.titleize
},
verify: false,
basic_auth: auth)
end
......@@ -54,7 +60,7 @@ class WebHook < ActiveRecord::Base
false
end
def async_execute(data)
Sidekiq::Client.enqueue(ProjectWebHookWorker, id, data)
def async_execute(data, hook_name)
Sidekiq::Client.enqueue(ProjectWebHookWorker, id, data, hook_name)
end
end
......@@ -63,149 +63,9 @@ class Note < ActiveRecord::Base
after_update :set_references
class << self
def create_status_change_note(noteable, project, author, status, source)
body = "Status changed to #{status}#{' by ' + source.gfm_reference if source}"
create(
noteable: noteable,
project: project,
author: author,
note: body,
system: true
)
end
# +noteable+ was referenced from +mentioner+, by including GFM in either
# +mentioner+'s description or an associated Note.
# Create a system Note associated with +noteable+ with a GFM back-reference
# to +mentioner+.
def create_cross_reference_note(noteable, mentioner, author)
gfm_reference = mentioner_gfm_ref(noteable, mentioner)
project = noteable.project
note_options = {
project: project,
author: author,
note: cross_reference_note_content(gfm_reference),
system: true
}
if noteable.kind_of?(Commit)
note_options.merge!(noteable_type: 'Commit', commit_id: noteable.id)
else
note_options.merge!(noteable: noteable)
end
if noteable.is_a?(ExternalIssue)
project.issues_tracker.create_cross_reference_note(noteable, mentioner, author)
else
create(note_options) unless cross_reference_disallowed?(noteable, mentioner)
end
end
def create_milestone_change_note(noteable, project, author, milestone)
body = if milestone.nil?
'Milestone removed'
else
"Milestone changed to #{milestone.title}"
end
create(
noteable: noteable,
project: project,
author: author,
note: body,
system: true
)
end
def create_assignee_change_note(noteable, project, author, assignee)
body = assignee.nil? ? 'Assignee removed' : "Reassigned to @#{assignee.username}"
create({
noteable: noteable,
project: project,
author: author,
note: body,
system: true
})
end
def create_labels_change_note(noteable, project, author, added_labels, removed_labels)
labels_count = added_labels.count + removed_labels.count
added_labels = added_labels.map{ |label| "~#{label.id}" }.join(' ')
removed_labels = removed_labels.map{ |label| "~#{label.id}" }.join(' ')
message = ''
if added_labels.present?
message << "added #{added_labels}"
end
if added_labels.present? && removed_labels.present?
message << ' and '
end
if removed_labels.present?
message << "removed #{removed_labels}"
end
message << ' ' << 'label'.pluralize(labels_count)
body = "#{message.capitalize}"
create(
noteable: noteable,
project: project,
author: author,
note: body,
system: true
)
end
def create_new_commits_note(merge_request, project, author, new_commits, existing_commits = [], oldrev = nil)
total_count = new_commits.length + existing_commits.length
commits_text = ActionController::Base.helpers.pluralize(total_count, 'commit')
body = "Added #{commits_text}:\n\n"
if existing_commits.length > 0
commit_ids =
if existing_commits.length == 1
existing_commits.first.short_id
else
if oldrev
"#{Commit.truncate_sha(oldrev)}...#{existing_commits.last.short_id}"
else
"#{existing_commits.first.short_id}..#{existing_commits.last.short_id}"
end
end
commits_text = ActionController::Base.helpers.pluralize(existing_commits.length, 'commit')
branch =
if merge_request.for_fork?
"#{merge_request.target_project_namespace}:#{merge_request.target_branch}"
else
merge_request.target_branch
end
message = "* #{commit_ids} - #{commits_text} from branch `#{branch}`"
body << message
body << "\n"
end
new_commits.each do |commit|
message = "* #{commit.short_id} - #{commit.title}"
body << message
body << "\n"
end
create(
noteable: merge_request,
project: project,
author: author,
note: body,
system: true
)
# TODO (rspeicher): Update usages
def create_cross_reference_note(*args)
SystemNoteService.cross_reference(*args)
end
def discussions_from_notes(notes)
......@@ -233,88 +93,19 @@ class Note < ActiveRecord::Base
[:discussion, type.try(:underscore), id, line_code].join("-").to_sym
end
# Determine if cross reference note should be created.
# eg. mentioning a commit in MR comments which exists inside a MR
# should not create "mentioned in" note.
def cross_reference_disallowed?(noteable, mentioner)
if mentioner.kind_of?(MergeRequest)
mentioner.commits.map(&:id).include? noteable.id
end
end
# Determine whether or not a cross-reference note already exists.
def cross_reference_exists?(noteable, mentioner)
gfm_reference = mentioner_gfm_ref(noteable, mentioner, true)
notes = if noteable.is_a?(Commit)
where(commit_id: noteable.id, noteable_type: 'Commit')
else
where(noteable_id: noteable.id, noteable_type: noteable.class)
end
notes.where('note like ?', cross_reference_note_pattern(gfm_reference)).
system.any?
end
def search(query)
where("note like :query", query: "%#{query}%")
end
end
def cross_reference_note_prefix
'mentioned in '
end
private
def cross_reference_note_content(gfm_reference)
cross_reference_note_prefix + "#{gfm_reference}"
end
def cross_reference_note_pattern(gfm_reference)
# Older cross reference notes contained underscores for emphasis
"%" + cross_reference_note_content(gfm_reference) + "%"
end
# Prepend the mentioner's namespaced project path to the GFM reference for
# cross-project references. For same-project references, return the
# unmodified GFM reference.
def mentioner_gfm_ref(noteable, mentioner, cross_reference = false)
if mentioner.is_a?(Commit) && cross_reference
return mentioner.gfm_reference.sub('commit ', 'commit %')
end
full_gfm_reference(mentioner.project, noteable.project, mentioner)
end
# Return the +mentioner+ GFM reference. If the mentioner and noteable
# projects are not the same, add the mentioning project's path to the
# returned value.
def full_gfm_reference(mentioning_project, noteable_project, mentioner)
if mentioning_project == noteable_project
mentioner.gfm_reference
else
if mentioner.is_a?(Commit)
mentioner.gfm_reference.sub(
/(commit )/,
"\\1#{mentioning_project.path_with_namespace}@"
)
else
mentioner.gfm_reference.sub(
/(issue |merge request )/,
"\\1#{mentioning_project.path_with_namespace}"
)
end
end
end
def cross_reference?
system && SystemNoteService.cross_reference?(note)
end
def max_attachment_size
current_application_settings.max_attachment_size.megabytes.to_i
end
def cross_reference?
note.start_with?(self.class.cross_reference_note_prefix)
end
def find_diff
return nil unless noteable && noteable.diffs.present?
......@@ -455,16 +246,6 @@ class Note < ActiveRecord::Base
@discussion_id ||= Note.build_discussion_id(noteable_type, noteable_id || commit_id, line_code)
end
# Returns true if this is a downvote note,
# otherwise false is returned
def downvote?
votable? && (note.start_with?('-1') ||
note.start_with?(':-1:') ||
note.start_with?(':thumbsdown:') ||
note.start_with?(':thumbs_down_sign:')
)
end
def for_commit?
noteable_type == "Commit"
end
......@@ -506,14 +287,18 @@ class Note < ActiveRecord::Base
nil
end
# Returns true if this is an upvote note,
# otherwise false is returned
DOWNVOTES = %w(-1 :-1: :thumbsdown: :thumbs_down_sign:)
# Check if the note is a downvote
def downvote?
votable? && note.start_with?(*DOWNVOTES)
end
UPVOTES = %w(+1 :+1: :thumbsup: :thumbs_up_sign:)
# Check if the note is an upvote
def upvote?
votable? && (note.start_with?('+1') ||
note.start_with?(':+1:') ||
note.start_with?(':thumbsup:') ||
note.start_with?(':thumbs_up_sign:')
)
votable? && note.start_with?(*UPVOTES)
end
def superceded?(notes)
......
......@@ -149,7 +149,7 @@ class Project < ActiveRecord::Base
validates :star_count, numericality: { greater_than_or_equal_to: 0 }
validate :check_limit, on: :create
validate :avatar_type,
if: ->(project) { project.avatar && project.avatar_changed? }
if: ->(project) { project.avatar.present? && project.avatar_changed? }
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
mount_uploader :avatar, AvatarUploader
......@@ -496,7 +496,7 @@ class Project < ActiveRecord::Base
def execute_hooks(data, hooks_scope = :push_hooks)
hooks.send(hooks_scope).each do |hook|
hook.async_execute(data)
hook.async_execute(data, hooks_scope.to_s)
end
if group
group.hooks.send(hooks_scope).each do |hook|
......
......@@ -50,6 +50,11 @@
# bitbucket_access_token :string(255)
# bitbucket_access_token_secret :string(255)
# location :string(255)
# encrypted_otp_secret :string(255)
# encrypted_otp_secret_iv :string(255)
# encrypted_otp_secret_salt :string(255)
# otp_required_for_login :boolean
# otp_backup_codes :text
# public_email :string(255) default(""), not null
#
......@@ -70,8 +75,14 @@ class User < ActiveRecord::Base
default_value_for :hide_no_password, false
default_value_for :theme_id, gitlab_config.default_theme
devise :database_authenticatable, :lockable, :async,
:recoverable, :rememberable, :trackable, :validatable, :omniauthable, :confirmable, :registerable
devise :two_factor_authenticatable,
otp_secret_encryption_key: File.read(Rails.root.join('.secret')).chomp
devise :two_factor_backupable, otp_number_of_backup_codes: 10
serialize :otp_backup_codes, JSON
devise :lockable, :async, :recoverable, :rememberable, :trackable,
:validatable, :omniauthable, :confirmable, :registerable
attr_accessor :force_random_password
......@@ -137,7 +148,7 @@ class User < ActiveRecord::Base
validates :notification_level, inclusion: { in: Notification.notification_levels }, presence: true
validate :namespace_uniq, if: ->(user) { user.username_changed? }
validate :avatar_type, if: ->(user) { user.avatar_changed? }
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
validate :unique_email, if: ->(user) { user.email_changed? }
validate :owns_notification_email, if: ->(user) { user.notification_email_changed? }
validate :owns_public_email, if: ->(user) { user.public_email_changed? }
......@@ -325,7 +336,7 @@ class User < ActiveRecord::Base
if primary_email_record
primary_email_record.destroy
self.emails.create(email: self.email_was)
self.update_secondary_emails!
end
end
......@@ -466,7 +477,7 @@ class User < ActiveRecord::Base
end
def project_deploy_keys
DeployKey.in_projects(self.authorized_projects.pluck(:id))
DeployKey.unscoped.in_projects(self.authorized_projects.pluck(:id)).distinct(:id)
end
def accessible_deploy_keys
......
......@@ -31,6 +31,10 @@ class GitPushService
# Initial push to the default branch. Take the full history of that branch as "newly pushed".
@push_commits = project.repository.commits(newrev)
# Ensure HEAD points to the default branch in case it is not master
branch_name = Gitlab::Git.ref_name(ref)
project.change_head(branch_name)
# Set protection on the default branch if configured
if (current_application_settings.default_branch_protection != PROTECTION_NONE)
developers_can_push = current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_PUSH ? true : false
......
......@@ -2,17 +2,17 @@ class IssuableBaseService < BaseService
private
def create_assignee_note(issuable)
Note.create_assignee_change_note(
SystemNoteService.change_assignee(
issuable, issuable.project, current_user, issuable.assignee)
end
def create_milestone_note(issuable)
Note.create_milestone_change_note(
SystemNoteService.change_milestone(
issuable, issuable.project, current_user, issuable.milestone)
end
def create_labels_note(issuable, added_labels, removed_labels)
Note.create_labels_change_note(
SystemNoteService.change_label(
issuable, issuable.project, current_user, added_labels, removed_labels)
end
end
......@@ -14,7 +14,7 @@ module Issues
private
def create_note(issue, current_commit)
Note.create_status_change_note(issue, issue.project, current_user, issue.state, current_commit)
SystemNoteService.change_status(issue, issue.project, current_user, issue.state, current_commit)
end
end
end
......@@ -14,7 +14,7 @@ module Issues
private
def create_note(issue)
Note.create_status_change_note(issue, issue.project, current_user, issue.state, nil)
SystemNoteService.change_status(issue, issue.project, current_user, issue.state, nil)
end
end
end
......@@ -2,7 +2,7 @@ module MergeRequests
class BaseService < ::IssuableBaseService
def create_note(merge_request)
Note.create_status_change_note(merge_request, merge_request.target_project, current_user, merge_request.state, nil)
SystemNoteService.change_status(merge_request, merge_request.target_project, current_user, merge_request.state, nil)
end
def hook_data(merge_request, action)
......
......@@ -82,8 +82,9 @@ module MergeRequests
mr_commit_ids.include?(commit.id)
end
Note.create_new_commits_note(merge_request, merge_request.project,
@current_user, new_commits, existing_commits, @oldrev)
SystemNoteService.add_commits(merge_request, merge_request.project,
@current_user, new_commits,
existing_commits, @oldrev)
end
end
......
......@@ -7,12 +7,12 @@ class SystemHooksService
def execute_hooks(data)
SystemHook.all.each do |sh|
async_execute_hook sh, data
async_execute_hook(sh, data, 'system_hooks')
end
end
def async_execute_hook(hook, data)
Sidekiq::Client.enqueue(SystemHookWorker, hook.id, data)
def async_execute_hook(hook, data, hook_name)
Sidekiq::Client.enqueue(SystemHookWorker, hook.id, data, hook_name)
end
def build_event_data(model, event)
......
# SystemNoteService
#
# Used for creating system notes (e.g., when a user references a merge request
# from an issue, an issue's assignee changes, an issue is closed, etc.)
class SystemNoteService
# Called when commits are added to a Merge Request
#
# noteable - Noteable object
# project - Project owning noteable
# author - User performing the change
# new_commits - Array of Commits added since last push
# existing_commits - Array of Commits added in a previous push
# oldrev - TODO (rspeicher): I have no idea what this actually does
#
# See new_commit_summary and existing_commit_summary.
#
# Returns the created Note object
def self.add_commits(noteable, project, author, new_commits, existing_commits = [], oldrev = nil)
total_count = new_commits.length + existing_commits.length
commits_text = "#{total_count} commit".pluralize(total_count)
body = "Added #{commits_text}:\n\n"
body << existing_commit_summary(noteable, existing_commits, oldrev)
body << new_commit_summary(new_commits).join("\n")
create_note(noteable: noteable, project: project, author: author, note: body)
end
# Called when the assignee of a Noteable is changed or removed
#
# noteable - Noteable object
# project - Project owning noteable
# author - User performing the change
# assignee - User being assigned, or nil
#
# Example Note text:
#
# "Assignee removed"
#
# "Reassigned to @rspeicher"
#
# Returns the created Note object
def self.change_assignee(noteable, project, author, assignee)
body = assignee.nil? ? 'Assignee removed' : "Reassigned to @#{assignee.username}"
create_note(noteable: noteable, project: project, author: author, note: body)
end
# Called when one or more labels on a Noteable are added and/or removed
#
# noteable - Noteable object
# project - Project owning noteable
# author - User performing the change
# added_labels - Array of Labels added
# removed_labels - Array of Labels removed
#
# Example Note text:
#
# "Added ~1 and removed ~2 ~3 labels"
#
# "Added ~4 label"
#
# "Removed ~5 label"
#
# Returns the created Note object
def self.change_label(noteable, project, author, added_labels, removed_labels)
labels_count = added_labels.count + removed_labels.count
references = ->(label) { "~#{label.id}" }
added_labels = added_labels.map(&references).join(' ')
removed_labels = removed_labels.map(&references).join(' ')
body = ''
if added_labels.present?
body << "added #{added_labels}"
body << ' and ' if removed_labels.present?
end
if removed_labels.present?
body << "removed #{removed_labels}"
end
body << ' ' << 'label'.pluralize(labels_count)
body = "#{body.capitalize}"
create_note(noteable: noteable, project: project, author: author, note: body)
end
# Called when the milestone of a Noteable is changed
#
# noteable - Noteable object
# project - Project owning noteable
# author - User performing the change
# milestone - Milestone being assigned, or nil
#
# Example Note text:
#
# "Milestone removed"
#
# "Miletone changed to 7.11"
#
# Returns the created Note object
def self.change_milestone(noteable, project, author, milestone)
body = 'Milestone '
body += milestone.nil? ? 'removed' : "changed to #{milestone.title}"
create_note(noteable: noteable, project: project, author: author, note: body)
end
# Called when the status of a Noteable is changed
#
# noteable - Noteable object
# project - Project owning noteable
# author - User performing the change
# status - String status
# source - Mentionable performing the change, or nil
#
# Example Note text:
#
# "Status changed to merged"
#
# "Status changed to closed by bc17db76"
#
# Returns the created Note object
def self.change_status(noteable, project, author, status, source)
body = "Status changed to #{status}"
body += " by #{source.gfm_reference}" if source
create_note(noteable: noteable, project: project, author: author, note: body)
end
# Called when a Mentionable references a Noteable
#
# noteable - Noteable object being referenced
# mentioner - Mentionable object
# author - User performing the reference
#
# Example Note text:
#
# "Mentioned in #1"
#
# "Mentioned in !2"
#
# "Mentioned in 54f7727c"
#
# See cross_reference_note_content.
#
# Returns the created Note object
def self.cross_reference(noteable, mentioner, author)
return if cross_reference_disallowed?(noteable, mentioner)
gfm_reference = mentioner_gfm_ref(noteable, mentioner)
note_options = {
project: noteable.project,
author: author,
note: cross_reference_note_content(gfm_reference)
}
if noteable.kind_of?(Commit)
note_options.merge!(noteable_type: 'Commit', commit_id: noteable.id)
else
note_options.merge!(noteable: noteable)
end
if noteable.is_a?(ExternalIssue)
noteable.project.issues_tracker.create_cross_reference_note(noteable, mentioner, author)
else
create(note_options)
end
create_note(note_options)
end
def self.cross_reference?(note_text)
note_text.start_with?(cross_reference_note_prefix)
end
# Check if a cross-reference is disallowed
#
# This method prevents adding a "mentioned in !1" note on every single commit
# in a merge request.
#
# noteable - Noteable object being referenced
# mentioner - Mentionable object
#
# Returns Boolean
def self.cross_reference_disallowed?(noteable, mentioner)
return false unless MergeRequest === mentioner
return false unless Commit === noteable
mentioner.commits.include?(noteable)
end
def self.cross_reference_exists?(noteable, mentioner)
# Initial scope should be system notes of this noteable type
notes = Note.system.where(noteable_type: noteable.class)
if noteable.is_a?(Commit)
# Commits have non-integer IDs, so they're stored in `commit_id`
notes = notes.where(commit_id: noteable.id)
else
notes = notes.where(noteable_id: noteable.id)
end
gfm_reference = mentioner_gfm_ref(noteable, mentioner, true)
notes = notes.where(note: cross_reference_note_content(gfm_reference))
notes.count > 0
end
private
def self.create_note(args = {})
Note.create(args.merge(system: true))
end
# Prepend the mentioner's namespaced project path to the GFM reference for
# cross-project references. For same-project references, return the
# unmodified GFM reference.
def self.mentioner_gfm_ref(noteable, mentioner, cross_reference = false)
# FIXME (rspeicher): This was breaking things.
# if mentioner.is_a?(Commit) && cross_reference
# return mentioner.gfm_reference.sub('commit ', 'commit %')
# end
full_gfm_reference(mentioner.project, noteable.project, mentioner)
end
# Return the +mentioner+ GFM reference. If the mentioner and noteable
# projects are not the same, add the mentioning project's path to the
# returned value.
def self.full_gfm_reference(mentioning_project, noteable_project, mentioner)
if mentioning_project == noteable_project
mentioner.gfm_reference
else
if mentioner.is_a?(Commit)
mentioner.gfm_reference.sub(
/(commit )/,
"\\1#{mentioning_project.path_with_namespace}@"
)
else
mentioner.gfm_reference.sub(
/(issue |merge request )/,
"\\1#{mentioning_project.path_with_namespace}"
)
end
end
end
def self.cross_reference_note_prefix
'mentioned in '
end
def self.cross_reference_note_content(gfm_reference)
"#{cross_reference_note_prefix}#{gfm_reference}"
end
# Build an Array of lines detailing each commit added in a merge request
#
# new_commits - Array of new Commit objects
#
# Returns an Array of Strings
def self.new_commit_summary(new_commits)
new_commits.collect do |commit|
"* #{commit.short_id} - #{commit.title}"
end
end
# Build a single line summarizing existing commits being added in a merge
# request
#
# noteable - MergeRequest object
# existing_commits - Array of existing Commit objects
# oldrev - Optional String SHA of ... TODO (rspeicher): I have no idea what this actually does.
#
# Examples:
#
# "* ea0f8418...2f4426b7 - 24 commits from branch `master`"
#
# "* ea0f8418..4188f0ea - 15 commits from branch `fork:master`"
#
# "* ea0f8418 - 1 commit from branch `feature`"
#
# Returns a newline-terminated String
def self.existing_commit_summary(noteable, existing_commits, oldrev = nil)
return '' if existing_commits.empty?
count = existing_commits.size
commit_ids = if count == 1
existing_commits.first.short_id
else
if oldrev
"#{Commit.truncate_sha(oldrev)}...#{existing_commits.last.short_id}"
else
"#{existing_commits.first.short_id}..#{existing_commits.last.short_id}"
end
end
commits_text = "#{count} commit".pluralize(count)
branch = noteable.target_branch
branch = "#{noteable.target_project_namespace}:#{branch}" if noteable.for_fork?
"* #{commit_ids} - #{commits_text} from branch `#{branch}`\n"
end
end
class TestHookService
def execute(hook, current_user)
data = Gitlab::PushDataBuilder.build_sample(project(hook), current_user)
hook.execute(data)
hook.execute(data, 'push_hooks')
end
private
......
%div
.login-box
.login-heading
%h3 Two-factor Authentication
.login-body
= form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f|
= f.text_field :otp_attempt, class: 'form-control', placeholder: 'Two-factor authentication code', required: true, autofocus: true
%p.help-block.hint If you've lost your phone, you may enter one of your recovery codes.
.prepend-top-20
= f.submit "Verify code", class: "btn btn-save"
......@@ -17,15 +17,27 @@
- few_commits.each do |commit|
= render "events/commit", commit: commit, project: project
- create_mr = current_user == event.author && event.new_ref? && create_mr_button?(event.project.default_branch, event.ref_name, event.project)
- if event.commits_count > 1
%li.commits-stat
- if event.commits_count > 2
%span ... and #{event.commits_count - 2} more commits.
- if event.md_ref?
- from = event.commit_from
- from_label = truncate_sha(from)
- else
- from = event.project.default_branch
- from_label = from
= link_to namespace_project_compare_path(event.project.namespace, event.project, from: from, to: event.commit_to) do
%strong Compare &rarr; #{from_label}...#{truncate_sha(event.commit_to)}
Compare #{from_label}...#{truncate_sha(event.commit_to)}
- if create_mr
or
= link_to create_mr_path(event.project.default_branch, event.ref_name, event.project) do
create a merge request
- elsif create_mr
%li.commits-stat
= link_to create_mr_path(event.project.default_branch, event.ref_name, event.project) do
Create Merge Request
......@@ -4,7 +4,7 @@
= icon('user fw')
%span
Profile
= nav_link(controller: :accounts) do
= nav_link(controller: [:accounts, :two_factor_auths]) do
= link_to profile_account_path, title: 'Account', data: {placement: 'right'} do
= icon('gear fw')
%span
......
......@@ -26,6 +26,33 @@
%span You don`t have one yet. Click generate to fix it.
= f.submit 'Generate', class: "btn success btn-build-token"
- unless current_user.ldap_user?
%fieldset
- if current_user.otp_required_for_login
%legend.text-success
= icon('check')
Two-factor Authentication enabled
%div
.pull-right
= link_to 'Disable Two-factor Authentication', profile_two_factor_auth_path, method: :delete, class: 'btn btn-close btn-sm',
data: { confirm: 'Are you sure?' }
%p
If you lose your recovery codes you can
%strong
= succeed ',' do
= link_to 'generate new ones', codes_profile_two_factor_auth_path, method: :post, data: { confirm: 'Are you sure?' }
invalidating all previous codes.
- else
%legend Two-factor Authentication
%div
%p
Increase your account's security by enabling two-factor authentication (2FA).
%p
Each time you log in you’ll be required to provide your username and
password as usual, plus a randomly-generated code from your phone.
%div
= link_to 'Enable Two-factor Authentication', new_profile_two_factor_auth_path, class: 'btn btn-success'
- if show_profile_social_tab?
%fieldset
......@@ -38,7 +65,7 @@
class: "btn btn-lg #{'active' if oauth_active?(provider)}"
- if oauth_active?(provider)
= link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'btn btn-lg' do
%i.fa.fa-close
= icon('close')
- if show_profile_username_tab?
%fieldset.update-username
......@@ -52,7 +79,7 @@
&nbsp;
.loading-gif.hide
%p
%i.fa.fa-spinner.fa-spin
= icon('spinner spin')
Saving new username
%p.light
= user_url(@user)
......
%p.slead
Should you ever lose your phone, each of these recovery codes can be used one
time each to regain access to your account. Please save them in a safe place.
.codes.well
%ul
- @codes.each do |code|
%li
%span.monospace= code
= link_to 'Proceed', profile_account_path, class: 'btn btn-success'
- page_title 'Recovery Codes', 'Two-factor Authentication'
%h3.page-title Two-factor Authentication Recovery codes
%hr
= render 'codes'
- page_title 'Two-factor Authentication', 'Account'
.alert.alert-success
Congratulations! You have enabled Two-factor Authentication!
= render 'codes'
- page_title 'Two-factor Authentication', 'Account'
%h2.page-title Two-Factor Authentication (2FA)
%p
Download the Google Authenticator application from App Store for iOS or
Google Play for Android and scan this code.
%hr
= form_tag profile_two_factor_auth_path, method: :post, class: 'form-horizontal' do |f|
- if @error
.alert.alert-danger
= @error
.form-group
.col-sm-2
.col-sm-10
= raw @qr_code
.form-group
= label_tag :pin_code, nil, class: "control-label"
.col-sm-10
= text_field_tag :pin_code, nil, class: "form-control", required: true, autofocus: true
.form-actions
= submit_tag 'Submit', class: 'btn btn-success'
.clearfix
.append-bottom-20
= render "shared/clone_panel"
- unless @project.empty_repo?
.well
%h4 Repository
%ul.nav.nav-pills
%li= link_to pluralize(number_with_delimiter(@repository.commit_count), 'commit'), namespace_project_commits_path(@project.namespace, @project, @ref || @repository.root_ref)
%li= link_to pluralize(number_with_delimiter(@repository.branch_names.count), 'branch'), namespace_project_branches_path(@project.namespace, @project)
%li= link_to pluralize(number_with_delimiter(@repository.tag_names.count), 'tag'), namespace_project_tags_path(@project.namespace, @project)
%h4.visibility-level-label
= visibility_level_icon(@project.visibility_level)
= "#{visibility_level_label(@project.visibility_level).capitalize} project"
- if @repository.changelog || @repository.license || @repository.contribution_guide
%ul.nav.nav-pills
- if @repository.changelog
%li.hidden-xs
= link_to changelog_url(@project) do
= icon("list-alt fw")
Changelog
- if @repository.license
%li
= link_to license_url(@project) do
= icon("check-circle-o fw")
License
- if @repository.contribution_guide
%li
= link_to contribution_guide_url(@project) do
= icon("info-circle fw")
Contribution guide
.actions
= link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: @ref || @repository.root_ref), class: 'btn btn-sm' do
%i.fa.fa-exchange
Compare code
- if can? current_user, :write_issue, @project
= link_to url_for_new_issue(@project, only_path: true), title: "New Issue", class: 'btn btn-sm append-right-10' do
= icon("exclamation-circle fw")
New Issue
- if can?(current_user, :download_code, @project)
&nbsp;
= render 'projects/repositories/download_archive', split_button: true, btn_class: 'btn-group-sm'
- if can? current_user, :write_merge_request, @project
= link_to new_namespace_project_merge_request_path(@project.namespace, @project), class: "btn btn-sm", title: "New Merge Request" do
= icon("plus fw")
New Merge Request
- if forked_from_project = @project.forked_from_project
.well
%h4
= icon("code-fork fw")
Forked from
.pull-right
= link_to forked_from_project.namespace.try(:name), project_path(forked_from_project)
- if version = @repository.version
.well
%h4
= icon("clock-o fw")
Version
.pull-right
= link_to version_url(@project) do
= @repository.blob_by_oid(version.id).data
- @project.ci_services.each do |ci_service|
- if ci_service.active? && ci_service.respond_to?(:builds_path)
.well
%h4
= icon("check fw")
= ci_service.title
.pull-right
- if ci_service.respond_to?(:status_img_path)
= link_to ci_service.builds_path, :'data-no-turbolink' => 'data-no-turbolink' do
= image_tag ci_service.status_img_path, alt: "build status"
- else
= link_to 'view builds', ci_service.builds_path, :'data-no-turbolink' => 'data-no-turbolink'
- unless @project.empty_repo?
.well
%h4 Contribute
%h4
= icon("archive fw")
Repository
%ul.nav.nav-pills
- if @repository.changelog
%li.hidden-xs
= link_to changelog_url(@project) do
Changelog
- if @repository.contribution_guide
%li.hidden-xs
= link_to contribution_guide_url(@project) do
Contribution guide
- if @repository.license
%li
= link_to license_url(@project) do
License
%li
= link_to namespace_project_commits_path(@project.namespace, @project, @ref || @repository.root_ref) do
= icon("history fw")
= pluralize(number_with_delimiter(@repository.commit_count), 'commit')
%li
= link_to namespace_project_branches_path(@project.namespace, @project) do
= icon("code-fork fw")
= pluralize(number_with_delimiter(@repository.branch_names.count), 'branch')
%li
= link_to namespace_project_tags_path(@project.namespace, @project) do
= icon("tags fw")
= pluralize(number_with_delimiter(@repository.tag_names.count), 'tag')
.actions
= link_to url_for_new_issue(@project, only_path: true), title: "New Issue", class: 'btn btn-sm' do
%i.fa.fa-fw.fa-exclamation-circle
New issue
= link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: @ref || @repository.root_ref), class: 'btn btn-sm append-right-10' do
%i.fa.fa-exchange
Compare code
- if can?(current_user, :download_code, @project)
= render 'projects/repositories/download_archive', split_button: true, btn_class: 'btn-group-sm'
= render "shared/clone_panel"
- if @project.archived?
.alert.alert-warning
%h4
%i.fa.fa-exclamation-triangle
= icon("exclamation-triangle fw")
Archived project!
%p Repository is read-only
- if @project.forked_from_project
.well
%i.fa.fa-code-fork.project-fork-icon
Forked from:
%br
= link_to @project.forked_from_project.name_with_namespace, project_path(@project.forked_from_project)
- if version = @repository.version
.well
%h4
Version
.pull-right
= link_to version_url(@project) do
= @repository.blob_by_oid(version.id).data
- @project.ci_services.each do |ci_service|
- if ci_service.active? && ci_service.respond_to?(:builds_path)
.well
%h4
= ci_service.title
.pull-right
- if ci_service.respond_to?(:status_img_path)
= link_to ci_service.builds_path, :'data-no-turbolink' => 'data-no-turbolink' do
= image_tag ci_service.status_img_path, alt: "build status"
- else
= link_to 'view builds', ci_service.builds_path, :'data-no-turbolink' => 'data-no-turbolink'
%ul.nav.nav-tabs
%li.active
= link_to '#tab-activity', 'data-toggle' => 'tab' do
= icon("tachometer")
Activity
- if @repository.readme
%li
= link_to '#tab-readme', 'data-toggle' => 'tab' do
= icon("file-text-o")
Readme
.tab-content
.tab-pane.active#tab-activity
......@@ -26,9 +28,10 @@
- if readme = @repository.readme
.tab-pane#tab-readme
%article.readme-holder#README
= link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)) do
%h4.readme-file-title
%i.fa.fa-file
= readme.name
.clearfix
%small.pull-right
= link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)), class: 'light' do
%i.fa.fa-file
= readme.name
.wiki
= render_readme(readme)
......@@ -10,16 +10,19 @@
%i.fa.fa-lock
protected
.pull-right
- if can?(current_user, :download_code, @project)
= render 'projects/repositories/download_archive', ref: branch.name, btn_class: 'btn-grouped btn-group-xs'
- if create_mr_button?(@repository.root_ref, branch.name)
= link_to create_mr_path(@repository.root_ref, branch.name), class: 'btn btn-grouped btn-xs' do
= icon('plus')
Merge Request
- if branch.name != @repository.root_ref
= link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: 'btn btn-grouped btn-xs', method: :post, title: "Compare" do
%i.fa.fa-files-o
= icon("exchange")
Compare
- if can_remove_branch?(@project, branch.name)
= link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-grouped btn-xs btn-remove remove-row', method: :delete, data: { confirm: 'Removed branch cannot be restored. Are you sure?'}, remote: true do
%i.fa.fa-trash-o
= icon("trash-o")
- if commit
%ul.list-unstyled
......
%ul.nav.nav-tabs
= nav_link(controller: [:commit, :commits]) do
= link_to namespace_project_commits_path(@project.namespace, @project, @repository.root_ref) do
= link_to namespace_project_commits_path(@project.namespace, @project, @ref || @repository.root_ref) do
= icon("history")
Commits
%span.badge= number_with_precision(@repository.commit_count, precision: 0, delimiter: ',')
= nav_link(controller: :compare) do
= link_to 'Compare', namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: @ref || @repository.root_ref)
= link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: @ref || @repository.root_ref) do
= icon("exchange")
Compare
= nav_link(html_options: {class: branches_tab_class}) do
= link_to namespace_project_branches_path(@project.namespace, @project) do
= icon("code-fork")
Branches
%span.badge.js-totalbranch-count= @repository.branches.size
= nav_link(controller: :tags) do
= link_to namespace_project_tags_path(@project.namespace, @project) do
= icon("tags")
Tags
%span.badge.js-totaltags-count= @repository.tags.length
......@@ -8,11 +8,17 @@
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'commits'
- if current_user && current_user.private_token
.commits-feed-holder.hidden-xs.hidden-sm
= link_to namespace_project_commits_path(@project.namespace, @project, @ref, {format: :atom, private_token: current_user.private_token}), title: "Feed", class: 'btn' do
%i.fa.fa-rss
Commits feed
.commits-feed-holder.hidden-xs.hidden-sm
- if create_mr_button?(@repository.root_ref, @ref)
= link_to create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success' do
= icon('plus')
Create Merge Request
- if current_user && current_user.private_token
= link_to namespace_project_commits_path(@project.namespace, @project, @ref, {format: :atom, private_token: current_user.private_token}), title: "Feed", class: 'prepend-left-10 btn' do
= icon("rss")
Commits Feed
%ul.breadcrumb.repo-breadcrumb
= commits_breadcrumbs
......
......@@ -13,9 +13,10 @@
= text_field_tag :to, params[:to], class: "form-control"
&nbsp;
= button_tag "Compare", class: "btn btn-create commits-compare-btn"
- if compare_to_mr_button?
= link_to compare_mr_path, class: 'prepend-left-10 btn' do
%strong Make a merge request
- if create_mr_button?
= link_to create_mr_path, class: 'prepend-left-10 btn' do
= icon("plus")
Create Merge Request
:javascript
......
......@@ -5,11 +5,13 @@
= parallel_diff_btn
= render 'projects/diffs/stats', diffs: diffs
- if show_diff_size_warning?(diffs)
= render 'projects/diffs/warning', diffs: diffs
- diff_files = safe_diff_files(diffs)
- if diff_files.count < diffs.size
= render 'projects/diffs/warning', diffs: diffs, shown_files_count: diff_files.count
.files
- safe_diff_files(diffs).each_with_index do |diff_file, index|
- diff_files.each_with_index do |diff_file, index|
= render 'projects/diffs/file', diff_file: diff_file, i: index, project: project
- if @diff_timeout
......
......@@ -14,6 +14,6 @@
= link_to "Email patch", merge_request_path(@merge_request, format: :patch), class: "btn btn-warning btn-sm"
%p
To preserve performance only
%strong #{allowed_diff_size} of #{diffs.size}
%strong #{shown_files_count} of #{diffs.size}
files are displayed.
- content_for :note_actions do
- if can?(current_user, :modify_issue, @issue)
- if @issue.closed?
= link_to 'Reopen Issue', issue_path(@issue, issue: {state_event: :reopen }, status_only: true), method: :put, class: "btn btn-grouped btn-reopen js-note-target-reopen", title: 'Reopen Issue'
= link_to 'Reopen Issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true), method: :put, class: 'btn btn-grouped btn-reopen js-note-target-reopen', title: 'Reopen Issue'
- else
= link_to 'Close Issue', issue_path(@issue, issue: {state_event: :close }, status_only: true), method: :put, class: "btn btn-grouped btn-close js-note-target-close", title: "Close Issue"
= link_to 'Close Issue', issue_path(@issue, issue: {state_event: :close}, status_only: true), method: :put, class: 'btn btn-grouped btn-close js-note-target-close', title: 'Close Issue'
= render 'shared/show_aside'
......@@ -15,11 +15,11 @@
%span= pluralize(@issue.participants(current_user).count, 'participant')
- @issue.participants(current_user).each do |participant|
= link_to_member(@project, participant, name: false, size: 24)
.voting_notes#notes= render "projects/notes/notes_with_form"
.voting_notes#notes= render 'projects/notes/notes_with_form'
%aside.col-md-3
.issuable-affix
.clearfix
%span.slead.has_tooltip{:"data-original-title" => 'Cross-project reference'}
%span.slead.has_tooltip{title: 'Cross-project reference'}
= cross_project_reference(@project, @issue)
%hr
.context
......
= form_for [@project.namespace.becomes(Namespace), @project, @issue], remote: true, html: {class: 'edit-issue inline-update'} do |f|
= form_for [@project.namespace.becomes(Namespace), @project, @issue], remote: true, html: {class: 'edit-issue inline-update js-issue-update'} do |f|
%div.prepend-top-20
.issuable-context-title
%label
......
......@@ -13,17 +13,17 @@
.pull-right
- if can?(current_user, :write_issue, @project)
= link_to new_namespace_project_issue_path(@project.namespace, @project), class: "btn btn-grouped new-issue-link", title: "New Issue", id: "new_issue_link" do
%i.fa.fa-plus
= link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'btn btn-grouped new-issue-link', title: 'New Issue', id: 'new_issue_link' do
= icon('plus')
New Issue
- if can?(current_user, :modify_issue, @issue)
- if @issue.closed?
= link_to 'Reopen', issue_path(@issue, issue: {state_event: :reopen }, status_only: true), method: :put, class: "btn btn-grouped btn-reopen"
= link_to 'Reopen', issue_path(@issue, issue: {state_event: :reopen}, status_only: true), method: :put, class: 'btn btn-grouped btn-reopen'
- else
= link_to 'Close', issue_path(@issue, issue: {state_event: :close }, status_only: true), method: :put, class: "btn btn-grouped btn-close", title: "Close Issue"
= link_to 'Close', issue_path(@issue, issue: {state_event: :close}, status_only: true), method: :put, class: 'btn btn-grouped btn-close', title: 'Close Issue'
= link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: "btn btn-grouped issuable-edit" do
%i.fa.fa-pencil-square-o
= link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'btn btn-grouped issuable-edit' do
= icon('pencil-square-o')
Edit
%hr
......@@ -31,11 +31,13 @@
= gfm escape_once(@issue.title)
%div
- if @issue.description.present?
.description
.description{class: can?(current_user, :modify_issue, @issue) ? 'js-task-list-container' : ''}
.wiki
= preserve do
= markdown(@issue.description, parse_tasks: true)
= markdown(@issue.description)
%textarea.hidden.js-task-list-field
= @issue.description
%hr
.issue-discussion
= render "projects/issues/discussion"
= render 'projects/issues/discussion'
%h3.page-title Compare branches for new Merge Request
%hr
%p.lead Compare branches for new Merge Request
= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], url: new_namespace_project_merge_request_path(@project.namespace, @project), method: :get, html: { class: "merge-request-form form-inline" } do |f|
.hide.alert.alert-danger.mr-compare-errors
......@@ -52,8 +51,8 @@
are the same.
%hr
= f.submit 'Compare branches', class: "btn btn-primary mr-compare-btn"
%div
= f.submit 'Compare branches', class: "btn btn-new mr-compare-btn"
:javascript
var source_branch = $("#merge_request_source_branch")
......
......@@ -7,78 +7,15 @@
%strong.label-branch #{@merge_request.target_project_namespace}:#{@merge_request.target_branch}
%span.pull-right
= link_to 'Change branches', new_namespace_project_merge_request_path(@project.namespace, @project)
= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: "merge-request-form form-horizontal gfm-form" } do |f|
= link_to 'Change branches', mr_change_branches_path(@merge_request)
%hr
= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal gfm-form' } do |f|
.merge-request-form-info
.form-group
= f.label :title, class: 'control-label' do
%strong Title *
.col-sm-10
= f.text_field :title, maxlength: 255, autofocus: true, class: 'form-control pad js-gfm-input', required: true
.form-group.issuable-description
= f.label :description, 'Description', class: 'control-label'
.col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "wiki" } do
= render 'projects/zen', f: f, attr: :description, classes: 'description form-control'
.col-sm-12-hint
.pull-left
Parsed with
#{link_to 'Gitlab Flavored Markdown', help_page_path('markdown', 'markdown'), target: '_blank'}.
.pull-right
Attach files by dragging &amp; dropping
or #{link_to 'selecting them', '#', class: 'markdown-selector'}.
.clearfix
.error-alert
%hr
.form-group
.issue-assignee
= f.label :assignee_id, class: 'control-label' do
%i.fa.fa-user
Assign to
.col-sm-10
= users_select_tag('merge_request[assignee_id]', placeholder: 'Select a user', class: 'custom-form-control', selected: @merge_request.assignee_id, project_id: @merge_request.target_project_id)
&nbsp;
= link_to 'Assign to me', '#', class: 'btn assign-to-me-link'
.form-group
.issue-milestone
= f.label :milestone_id, class: 'control-label' do
%i.fa.fa-clock-o
Milestone
.col-sm-10
- if milestone_options(@merge_request).present?
= f.select(:milestone_id, milestone_options(@merge_request), {include_blank: 'Select milestone'}, {class: 'select2'})
- else
%span.light No open milestones available.
&nbsp;
- if can? current_user, :admin_milestone, @merge_request.target_project
= link_to 'Create new milestone', new_namespace_project_milestone_path(@merge_request.target_project.namespace, @merge_request.target_project), target: :blank
.form-group
= f.label :label_ids, class: 'control-label' do
%i.fa.fa-tag
Labels
.col-sm-10
- if @merge_request.target_project.labels.any?
= f.collection_select :label_ids, @merge_request.target_project.labels.all, :id, :name, {selected: @merge_request.label_ids}, multiple: true, class: 'select2'
- else
%span.light No labels yet.
&nbsp;
- if can? current_user, :admin_label, @merge_request.target_project
= link_to 'Create new label', new_namespace_project_label_path(@merge_request.target_project.namespace, @merge_request.target_project), target: :blank
.form-actions
- if guide_url = contribution_guide_url(@target_project)
%p
Please review the
%strong #{link_to 'guidelines for contribution', guide_url}
to this repository.
= f.hidden_field :source_project_id
= f.hidden_field :source_branch
= f.hidden_field :target_project_id
= f.hidden_field :target_branch
= f.submit 'Submit merge request', class: 'btn btn-create'
= render 'projects/issuable_form', f: f, issuable: @merge_request
= f.hidden_field :source_project_id
= f.hidden_field :source_branch
= f.hidden_field :target_project_id
= f.hidden_field :target_branch
.mr-compare.merge-request
%ul.nav.nav-tabs.merge-request-tabs
......
= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], remote: true, html: {class: 'edit-merge_request inline-update'} do |f|
= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], remote: true, html: {class: 'edit-merge_request inline-update js-merge-request-update'} do |f|
%div.prepend-top-20
.issuable-context-title
%label
......@@ -19,13 +19,13 @@
%span.back-to-milestone
= link_to namespace_project_milestone_path(@project.namespace, @project, @merge_request.milestone) do
%strong
%i.fa.fa-clock-o
= icon('clock-o')
= @merge_request.milestone.title
- else
none
.issuable-context-selectbox
- if can?(current_user, :modify_merge_request, @merge_request)
= f.select(:milestone_id, milestone_options(@merge_request), { include_blank: "Select milestone" }, {class: 'select2 select2-compact js-select2 js-milestone'})
= f.select(:milestone_id, milestone_options(@merge_request), { include_blank: 'Select milestone' }, {class: 'select2 select2-compact js-select2 js-milestone'})
= hidden_field_tag :merge_request_context
= f.submit class: 'btn'
......@@ -35,13 +35,13 @@
%label
Subscription:
%button.btn.btn-block.subscribe-button{:type => 'button'}
%i.fa.fa-eye
%span= @merge_request.subscribed?(current_user) ? "Unsubscribe" : "Subscribe"
- subscribtion_status = @merge_request.subscribed?(current_user) ? "subscribed" : "unsubscribed"
.subscription-status{"data-status" => subscribtion_status}
.description-block.unsubscribed{class: ( "hidden" if @merge_request.subscribed?(current_user) )}
= icon('eye')
%span= @merge_request.subscribed?(current_user) ? 'Unsubscribe' : 'Subscribe'
- subscribtion_status = @merge_request.subscribed?(current_user) ? 'subscribed' : 'unsubscribed'
.subscription-status{data: {status: subscribtion_status}}
.description-block.unsubscribed{class: ( 'hidden' if @merge_request.subscribed?(current_user) )}
You're not receiving notifications from this thread.
.description-block.subscribed{class: ( "hidden" unless @merge_request.subscribed?(current_user) )}
.description-block.subscribed{class: ( 'hidden' unless @merge_request.subscribed?(current_user) )}
You're receiving notifications because you're subscribed to this thread.
:coffeescript
......
......@@ -3,7 +3,9 @@
%div
- if @merge_request.description.present?
.description
.description{class: can?(current_user, :modify_merge_request, @merge_request) ? 'js-task-list-container' : ''}
.wiki
= preserve do
= markdown(@merge_request.description, parse_tasks: true)
= markdown(@merge_request.description)
%textarea.hidden.js-task-list-field
= @merge_request.description
.note-edit-form
= form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true do |f|
= note_target_fields(note)
= render layout: 'projects/md_preview', locals: { preview_class: "note-text" } do
= render 'projects/zen', f: f, attr: :note,
classes: 'note_text js-note-text'
= render layout: 'projects/md_preview', locals: { preview_class: 'note-text' } do
= render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text js-task-list-field'
.comment-hints.clearfix
.pull-left Comments are parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"),{ target: '_blank', tabindex: -1 }}
.pull-right Attach files by dragging &amp; dropping or #{link_to "selecting them", '#', class: 'markdown-selector', tabindex: -1 }.
.pull-left Comments are parsed with #{link_to 'GitLab Flavored Markdown', help_page_path('markdown', 'markdown'),{ target: '_blank', tabindex: -1 }}
.pull-right Attach files by dragging &amp; dropping or #{link_to 'selecting them', '#', class: 'markdown-selector', tabindex: -1 }.
.note-form-actions
.buttons
= f.submit 'Save Comment', class: "btn btn-primary btn-save btn-grouped js-comment-button"
= link_to 'Cancel', "#", class: "btn btn-cancel note-edit-cancel"
\ No newline at end of file
= f.submit 'Save Comment', class: 'btn btn-primary btn-save btn-grouped js-comment-button'
= link_to 'Cancel', '#', class: 'btn btn-cancel note-edit-cancel'
......@@ -2,29 +2,37 @@
.timeline-entry-inner
.timeline-icon
- if note.system
%span.fa.fa-circle
%span= icon('circle')
- else
= link_to user_path(note.author) do
= image_tag avatar_icon(note.author_email), class: "avatar s40", alt: ''
= image_tag avatar_icon(note.author_email), class: 'avatar s40', alt: ''
.timeline-content
.note-header
.note-actions
= link_to "##{dom_id(note)}", name: dom_id(note) do
%i.fa.fa-link
= link_to "##{dom_id(note)}", name: dom_id(note), title: "Link here" do
= icon('link fw')
Link here
&nbsp;
- if can?(current_user, :admin_note, note) && note.editable?
= link_to "#", title: "Edit comment", class: "js-note-edit" do
%i.fa.fa-pencil-square-o
- if note_editable?(note)
= link_to '#', title: 'Edit comment', class: 'js-note-edit' do
= icon('pencil-square-o fw')
Edit
&nbsp;
= link_to namespace_project_note_path(@project.namespace, @project, note), title: "Remove comment", method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: "danger js-note-delete" do
%i.fa.fa-trash-o.cred
= 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: 'danger js-note-delete' do
= icon('trash-o fw', class: 'cred')
Remove
- unless note.system
- member = note.project.team.find_member(note.author.id)
- if member
%span.note-role.label
= member.human_access
- if note.system
= link_to user_path(note.author) do
= image_tag avatar_icon(note.author_email), class: "avatar s16", alt: ''
= link_to_member(@project, note.author, avatar: false)
= image_tag avatar_icon(note.author_email), class: 'avatar s16', alt: ''
= link_to_member(note.project, note.author, avatar: false)
%span.author-username
= '@' + note.author.username
%span.note-last-update
......@@ -33,24 +41,24 @@
- if note.superceded?(@notes)
- if note.upvote?
%span.vote.upvote.label.label-gray.strikethrough
%i.fa.fa-thumbs-up
= icon('thumbs-up')
\+1
- if note.downvote?
%span.vote.downvote.label.label-gray.strikethrough
%i.fa.fa-thumbs-down
= icon('thumbs-down')
\-1
- else
- if note.upvote?
%span.vote.upvote.label.label-success
%i.fa.fa-thumbs-up
= icon('thumbs-up')
\+1
- if note.downvote?
%span.vote.downvote.label.label-danger
%i.fa.fa-thumbs-down
= icon('thumbs-down')
\-1
.note-body
.note-body{class: note_editable?(note) ? 'js-task-list-container' : ''}
.note-text
= preserve do
= markdown(note.note, {no_header_anchors: true})
......@@ -62,10 +70,10 @@
= link_to note.attachment.url, target: '_blank' do
= image_tag note.attachment.url, class: 'note-image-attach'
.attachment
= link_to note.attachment.url, target: "_blank" do
%i.fa.fa-paperclip
= link_to note.attachment.url, target: '_blank' do
= icon('paperclip')
= note.attachment_identifier
= link_to delete_attachment_namespace_project_note_path(@project.namespace, @project, note),
title: "Delete this attachment", method: :delete, remote: true, data: { confirm: 'Are you sure you want to remove the attachment?' }, class: "danger js-note-attachment-delete" do
%i.fa.fa-trash-o.cred
= link_to delete_attachment_namespace_project_note_path(note.project.namespace, note.project, note),
title: 'Delete this attachment', method: :delete, remote: true, data: { confirm: 'Are you sure you want to remove the attachment?' }, class: 'danger js-note-attachment-delete' do
= icon('trash-o', class: 'cred')
.clear
......@@ -8,7 +8,7 @@
- if can? current_user, :download_code, @project
.tree-download-holder
= render 'projects/repositories/download_archive', ref: @ref, btn_class: 'btn-group-sm pull-right hidden-xs hidden-sm', split_button: true
= render 'projects/repositories/download_archive', ref: @ref, btn_class: 'btn-group pull-right hidden-xs hidden-sm', split_button: true
#tree-holder.tree-holder.clearfix
= render "tree", tree: @tree
- project = project || @project
.git-clone-holder.input-group
.input-group-btn
%button{ |
:type => 'button', |
class: "btn #{ 'active' if default_clone_protocol == 'ssh' }#{ ' has_tooltip' if current_user && current_user.require_ssh_key? }", |
:"data-clone" => project.ssh_url_to_repo, |
:"data-title" => "Add an SSH key to your profile<br> to pull or push via SSH",
:"data-html" => "true",
:"data-container" => "body"}
SSH
%button{ |
:type => 'button', |
class: "btn #{ 'active' if default_clone_protocol == 'http' }#{ ' has_tooltip' if current_user && current_user.require_password? }", |
:"data-clone" => project.http_url_to_repo, |
:"data-title" => "Set a password on your account<br> to pull or push via #{gitlab_config.protocol.upcase}",
:"data-html" => "true",
:"data-container" => "body"}
= gitlab_config.protocol.upcase
= text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true
- if project.kind_of?(Project)
.input-group-addon.git-protocols
.input-group-btn
%button{ |
class: "btn btn-sm #{ 'active' if default_clone_protocol == 'ssh' }#{ ' has_tooltip' if current_user && current_user.require_ssh_key? }", |
:"data-clone" => project.ssh_url_to_repo, |
:"data-title" => "Add an SSH key to your profile<br> to pull or push via SSH",
:"data-html" => "true",
:"data-container" => "body"}
SSH
.input-group-btn
%button{ |
class: "btn btn-sm #{ 'active' if default_clone_protocol == 'http' }#{ ' has_tooltip' if current_user && current_user.require_password? }", |
:"data-clone" => project.http_url_to_repo, |
:"data-title" => "Set a password on your account<br> to pull or push via #{gitlab_config.protocol.upcase}",
:"data-html" => "true",
:"data-container" => "body"}
= gitlab_config.protocol.upcase
= text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control input-sm", readonly: true
- if project.kind_of?(Project) && project.empty_repo?
.input-group-addon
.visibility-level-label.has_tooltip{'data-title' => "#{visibility_level_label(project.visibility_level)} project" }
= visibility_level_icon(project.visibility_level)
......
......@@ -51,8 +51,6 @@
= button_tag "Update issues", class: "btn update_selected_issues btn-save"
:coffeescript
new UsersSelect()
$('form.filter-form').on 'submit', (event) ->
event.preventDefault()
Turbolinks.visit @.action + '&' + $(@).serialize()
......@@ -3,8 +3,8 @@ class ProjectWebHookWorker
sidekiq_options queue: :project_web_hook
def perform(hook_id, data)
def perform(hook_id, data, hook_name)
data = data.with_indifferent_access
WebHook.find(hook_id).execute(data)
WebHook.find(hook_id).execute(data, hook_name)
end
end
......@@ -3,7 +3,7 @@ class SystemHookWorker
sidekiq_options queue: :system_hook
def perform(hook_id, data)
SystemHook.find(hook_id).execute data
def perform(hook_id, data, hook_name)
SystemHook.find(hook_id).execute(data, hook_name)
end
end
......@@ -31,7 +31,7 @@ module Gitlab
config.encoding = "utf-8"
# Configure sensitive parameters which will be filtered from the log file.
config.filter_parameters.push(:password, :password_confirmation, :private_token)
config.filter_parameters.push(:password, :password_confirmation, :private_token, :otp_attempt)
# Enable escaping HTML in JSON.
config.active_support.escape_html_entities_in_json = true
......
......@@ -5,4 +5,5 @@ if Rails.env.development?
Rack::MiniProfilerRails.initialize!(Rails.application)
Rack::MiniProfiler.config.position = 'right'
Rack::MiniProfiler.config.start_hidden = true
Rack::MiniProfiler.config.skip_paths << '/specs'
end
# Use this hook to configure devise mailer, warden hooks and so forth. The first
# four configuration values can also be set straight in your models.
Devise.setup do |config|
config.warden do |manager|
manager.default_strategies(scope: :user).unshift :two_factor_authenticatable
manager.default_strategies(scope: :user).unshift :two_factor_backupable
end
# ==> Mailer Configuration
# Configure the class responsible to send e-mails.
config.mailer = "DeviseMailer"
......
......@@ -246,6 +246,11 @@ Gitlab::Application.routes.draw do
resources :keys
resources :emails, only: [:index, :create, :destroy]
resource :avatar, only: [:destroy]
resource :two_factor_auth, only: [:new, :create, :destroy] do
member do
post :codes
end
end
end
end
......
class AddDeviseTwoFactorToUsers < ActiveRecord::Migration
def change
add_column :users, :encrypted_otp_secret, :string
add_column :users, :encrypted_otp_secret_iv, :string
add_column :users, :encrypted_otp_secret_salt, :string
add_column :users, :otp_required_for_login, :boolean
end
end
class AddDeviseTwoFactorBackupableToUsers < ActiveRecord::Migration
def change
add_column :users, :otp_backup_codes, :text
end
end
class AddDefaultProjectVisibililtyToApplicationSettings < ActiveRecord::Migration
def change
def up
add_column :application_settings, :default_project_visibility, :integer
visibility = Settings.gitlab.default_projects_features['visibility_level']
execute("update application_settings set default_project_visibility = #{visibility}")
end
def down
remove_column :application_settings, :default_project_visibility
end
end
# This migration is a duplicate of 20150425164651_change_collation_for_tag_names.acts_as_taggable_on_engine.rb
# It shold be applied before the index additions to ensure that `name` is case sensitive.
class GitlabChangeCollationForTagNames < ActiveRecord::Migration
def up
if ActsAsTaggableOn::Utils.using_mysql?
execute("ALTER TABLE tags MODIFY name varchar(255) CHARACTER SET utf8 COLLATE utf8_bin;")
end
end
end
class RemoveDuplicateTags < ActiveRecord::Migration
def up
select_all("SELECT name, COUNT(id) as cnt FROM tags GROUP BY name HAVING COUNT(id) > 1").each do |tag|
tag_name = quote_string(tag["name"])
duplicate_ids = select_all("SELECT id FROM tags WHERE name = '#{tag_name}'").map{|tag| tag["id"]}
origin_tag_id = duplicate_ids.first
duplicate_ids.delete origin_tag_id
execute("UPDATE taggings SET tag_id = #{origin_tag_id} WHERE tag_id IN(#{duplicate_ids.join(",")})")
execute("DELETE FROM tags WHERE id IN(#{duplicate_ids.join(",")})")
end
end
def down
end
end
......@@ -3,8 +3,15 @@ class AddMissingUniqueIndices < ActiveRecord::Migration
def self.up
add_index :tags, :name, unique: true
remove_index :taggings, :tag_id
remove_index :taggings, [:taggable_id, :taggable_type, :context]
# pre-GitLab v6.7.0 may not have these indices since there were no
# migrations for them
if index_exists?(:taggings, :tag_id)
remove_index :taggings, :tag_id
end
if index_exists?(:taggings, [:taggable_id, :taggable_type, :context])
remove_index :taggings, [:taggable_id, :taggable_type, :context]
end
add_index :taggings,
[:tag_id, :taggable_id, :taggable_type, :context, :tagger_id, :tagger_type],
unique: true, name: 'taggings_idx'
......
class RemoveAbandonedGroupMembersRecords < ActiveRecord::Migration
def change
def up
execute("DELETE FROM members WHERE type = 'GroupMember' AND source_id NOT IN(\
SELECT id FROM namespaces WHERE type='Group')")
end
def down
end
end
# Convert legacy Markdown-emphasized notes to the current, non-emphasized format
#
# _mentioned in 54f7727c850972f0401c1312a7c4a6a380de5666_
#
# becomes
#
# mentioned in 54f7727c850972f0401c1312a7c4a6a380de5666
class ConvertLegacyReferenceNotes < ActiveRecord::Migration
def up
execute %q{UPDATE notes SET note = trim(both '_' from note) WHERE system = true AND note LIKE '\_%\_'}
end
def down
# noop
end
end
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20150507194350) do
ActiveRecord::Schema.define(version: 20150509180749) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......@@ -567,6 +567,11 @@ ActiveRecord::Schema.define(version: 20150507194350) do
t.string "bitbucket_access_token"
t.string "bitbucket_access_token_secret"
t.string "location"
t.string "encrypted_otp_secret"
t.string "encrypted_otp_secret_iv"
t.string "encrypted_otp_secret_salt"
t.boolean "otp_required_for_login"
t.text "otp_backup_codes"
t.string "public_email", default: "", null: false
end
......
# Development
# Development
- [Architecture](architecture.md) of GitLab
- [Shell commands](shell_commands.md) in the GitLab codebase
......@@ -6,3 +6,4 @@
- [CI setup](ci_setup.md) for testing GitLab
- [Sidekiq debugging](sidekiq_debugging.md)
- [UI guide](ui_guide.md) for building GitLab with existing css styles and elements
- [Migration Style Guide](migration_style_guide.md) for creating safe migrations
# Migration Style Guide
When writing migrations for GitLab, you have to take into account that
these will be ran by hundreds of thousands of organizations of all sizes, some with
many years of data in their database.
In addition, having to take a server offline for a an upgrade small or big is
a big burden for most organizations. For this reason it is important that your
migrations are written carefully, can be applied online and adhere to the style guide below.
When writing your migrations, also consider that databases might have stale data
or inconsistencies and guard for that. Try to make as little assumptions as possible
about the state of the database.
Please don't depend on GitLab specific code since it can change in future versions.
If needed copy-paste GitLab code into the migration to make make it forward compatible.
## Comments in the migration
Each migration you write needs to have the two following pieces of information
as comments.
### Online, Offline, errors?
First, you need to provide information on whether the migration can be applied:
1. online without errors (works on previous version and new one)
2. online with errors on old instances after migrating
3. online with errors on new instances while migrating
4. offline (needs to happen without app servers to prevent db corruption)
It is always preferable to have a migration run online. If you expect the migration
to take particularly long (for instance, if it loops through all notes),
this is valuable information to add.
### Reversibility
Your migration should be reversible. This is very important, as it should
be possible to downgrade in case of a vulnerability or bugs.
In your migration, add a comment describing how the reversibility of the
migration was tested.
## Removing indices
If you need to remove index, please add a condition like in following example:
```
remove_index :namespaces, column: :name if index_exists?(:namespaces, :name)
```
## Adding indices
If you need to add an unique index please keep in mind there is possibility of existing duplicates. If it is possible write a separate migration for handling this situation. It can be just removing or removing with overwriting all references to these duplicates depend on situation.
## Testing
Make sure that your migration works with MySQL and PostgreSQL with data. An empty database does not guarantee that your migration is correct.
Make sure your migration can be reversed.
## Data migration
Please prefer Arel and plain SQL over usual ActiveRecord syntax. In case of using plain SQL you need to quote all input manually with `quote_string` helper.
Example with Arel:
```
users = Arel::Table.new(:users)
users.group(users[:user_id]).having(users[:id].count.gt(5))
#updtae other tables with this results
```
Example with plain SQL and `quote_string` helper:
```
select_all("SELECT name, COUNT(id) as cnt FROM tags GROUP BY name HAVING COUNT(id) > 1").each do |tag|
tag_name = quote_string(tag["name"])
duplicate_ids = select_all("SELECT id FROM tags WHERE name = '#{tag_name}'").map{|tag| tag["id"]}
origin_tag_id = duplicate_ids.first
duplicate_ids.delete origin_tag_id
execute("UPDATE taggings SET tag_id = #{origin_tag_id} WHERE tag_id IN(#{duplicate_ids.join(",")})")
execute("DELETE FROM tags WHERE id IN(#{duplicate_ids.join(",")})")
end
```
......@@ -4,6 +4,12 @@
Since an installation from source is a lot of work and error prone we strongly recommend the fast and reliable [Omnibus package installation](https://about.gitlab.com/downloads/) (deb/rpm).
One reason the Omnibus package is more reliable is its use of Runit to restart any of the GitLab processes in case one crashes.
On heavily used GitLab instances the memory usage of the Sidekiq background worker will grow over time.
Omnibus packages solve this by [letting the Sidekiq terminate gracefully](http://doc.gitlab.com/ce/operations/sidekiq_memory_killer.html) if it uses too much memory.
After this termination Runit will detect Sidekiq is not running and will start it.
Since installations from source don't have Runit, Sidekiq can't be terminated and its memory usage will grow over time.
## Select Version to Install
Make sure you view [this installation guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md) from the tag (version) of GitLab you would like to install.
......@@ -56,7 +62,13 @@ up-to-date and install it.
Install the required packages (needed to compile Ruby and native extensions to Ruby gems):
sudo apt-get install -y build-essential zlib1g-dev libyaml-dev libssl-dev libgdbm-dev libreadline-dev libncurses5-dev libffi-dev curl openssh-server redis-server checkinstall libxml2-dev libxslt-dev libcurl4-openssl-dev libicu-dev logrotate python-docutils pkg-config cmake libkrb5-dev nodejs
sudo apt-get install -y build-essential zlib1g-dev libyaml-dev libssl-dev libgdbm-dev libreadline-dev libncurses5-dev libffi-dev curl openssh-server redis-server checkinstall libxml2-dev libxslt-dev libcurl4-openssl-dev libicu-dev logrotate python-docutils pkg-config cmake nodejs
If you want to use Kerberos for user authentication, then install libkrb5-dev:
sudo apt-get install libkrb5-dev
**Note:** If you don't know what Kerberos is, then you certainly don't need it.
Make sure you have the right version of Git installed
......@@ -270,10 +282,12 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da
**Note:** As of bundler 1.5.2, you can invoke `bundle install -jN` (where `N` the number of your processor cores) and enjoy the parallel gems installation with measurable difference in completion time (~60% faster). Check the number of your cores with `nproc`. For more information check this [post](http://robots.thoughtbot.com/parallel-gem-installing-using-bundler). First make sure you have bundler >= 1.5.2 (run `bundle -v`) as it addresses some [issues](https://devcenter.heroku.com/changelog-items/411) that were [fixed](https://github.com/bundler/bundler/pull/2817) in 1.5.2.
# For PostgreSQL (note, the option says "without ... mysql")
sudo -u git -H bundle install --deployment --without development test mysql aws
sudo -u git -H bundle install --deployment --without development test mysql aws kerberos
# Or if you use MySQL (note, the option says "without ... postgres")
sudo -u git -H bundle install --deployment --without development test postgres aws
sudo -u git -H bundle install --deployment --without development test postgres aws kerberos
**Note:** If you want to use Kerberos for user authentication, then omit `kerberos` in the `--without` option above.
### Install GitLab Shell
......
......@@ -172,7 +172,7 @@ GFM will turn that reference into a link so you can navigate between them easily
GFM will recognize the following:
| input | references |
|-----------------------:|:---------------------------|
|:-----------------------|:---------------------------|
| `@user_name` | specific user |
| `@group_name` | specific group |
| `@all` | entire team |
......@@ -189,7 +189,7 @@ GFM will recognize the following:
GFM also recognizes certain cross-project references:
| input | references |
|----------------------------------------:|:------------------------|
|:----------------------------------------|:------------------------|
| `namespace/project#123` | issue |
| `namespace/project!123` | merge request |
| `namespace/project$123` | snippet |
......@@ -198,15 +198,23 @@ GFM also recognizes certain cross-project references:
## Task Lists
You can add task lists to merge request and issue descriptions to keep track of to-do items. To create a task, add an unordered list to the description in an issue or merge request, formatted like so:
You can add task lists to issues, merge requests and comments. To create a task list, add a specially-formatted Markdown list, like so:
```no-highlight
* [x] Completed task
* [ ] Unfinished task
* [x] Nested task
- [x] Completed task
- [ ] Incomplete task
- [ ] Sub-task 1
- [x] Sub-task 2
- [ ] Sub-task 3
```
Task lists can only be created in descriptions, not in titles or comments. Task item state can be managed by editing the description's Markdown or by clicking the rendered checkboxes.
- [x] Completed task
- [ ] Incomplete task
- [ ] Sub-task 1
- [x] Sub-task 2
- [ ] Sub-task 3
Task lists can only be created in descriptions, not in titles. Task item state can be managed by editing the description's Markdown or by toggling the rendered check boxes.
# Standard Markdown
......@@ -246,51 +254,38 @@ Alt-H2
### Header IDs and links
All markdown rendered headers automatically get IDs, except for comments.
All Markdown-rendered headers automatically get IDs, except in comments.
On hover a link to those IDs becomes visible to make it easier to copy the link to the header to give it to someone else.
The IDs are generated from the content of the header according to the following rules:
1. remove the heading hashes `#` and process the rest of the line as it would be processed if it were not a header
2. from the result, remove all HTML tags, but keep their inner content
3. convert all characters to lowercase
4. convert all characters except `[a-z0-9_-]` into hyphens `-`
5. transform multiple adjacent hyphens into a single hyphen
6. remove trailing and heading hyphens
1. All text is converted to lowercase
1. All non-word text (e.g., punctuation, HTML) is removed
1. All spaces are converted to hyphens
1. Two or more hyphens in a row are converted to one
1. If a header with the same ID has already been generated, a unique
incrementing number is appended.
For example:
```
###### ..Ab_c-d. e [anchor](URL) ![alt text](URL)..
# This header has spaces in it
## This header has a :thumbsup: in it
# This header has Unicode in it: 한글
## This header has spaces in it
### This header has spaces in it
```
which renders as:
###### ..Ab_c-d. e [anchor](URL) ![alt text](URL)..
will first be converted by step 1) into a string like:
Would generate the following link IDs:
```
..Ab_c-d. e &lt;a href="URL">anchor&lt;/a> &lt;img src="URL" alt="alt text"/>..
```
1. `this-header-has-spaces-in-it`
1. `this-header-has-a-in-it`
1. `this-header-has-unicode-in-it-한글`
1. `this-header-has-spaces-in-it-1`
1. `this-header-has-spaces-in-it-2`
After removing the tags in step 2) we get:
```
..Ab_c-d. e anchor ..
```
And applying all the other steps gives the id:
```
ab_c-d-e-anchor
```
Note in particular how:
- for markdown anchors `[text](URL)`, only the `text` is used
- markdown images `![alt](URL)` are completely ignored
Note that the Emoji processing happens before the header IDs are generated, so the Emoji is converted to an image which then gets removed from the ID.
## Emphasis
......@@ -322,8 +317,6 @@ Strikethrough uses two tildes. ~~Scratch this.~~
1. Ordered sub-list
4. And another item.
Some text that should be aligned with the above item.
* Unordered list can use asterisks
- Or minuses
+ Or pluses
......@@ -336,8 +329,6 @@ Strikethrough uses two tildes. ~~Scratch this.~~
1. Ordered sub-list
4. And another item.
Some text that should be aligned with the above item.
* Unordered list can use asterisks
- Or minuses
+ Or pluses
......@@ -432,7 +423,7 @@ Quote break.
You can also use raw HTML in your Markdown, and it'll mostly work pretty well.
See the documentation for HTML::Pipeline's [SanitizationFilter](http://www.rubydoc.info/gems/html-pipeline/HTML/Pipeline/SanitizationFilter#WHITELIST-constant) class for the list of allowed HTML tags and attributes. In addition to the default `SanitizationFilter` whitelist, GitLab allows the `class`, `id`, and `style` attributes.
See the documentation for HTML::Pipeline's [SanitizationFilter](http://www.rubydoc.info/gems/html-pipeline/HTML/Pipeline/SanitizationFilter#WHITELIST-constant) class for the list of allowed HTML tags and attributes. In addition to the default `SanitizationFilter` whitelist, GitLab allows `span` elements.
```no-highlight
<dl>
......@@ -536,6 +527,20 @@ Code above produces next output:
The row of dashes between the table header and body must have at least three dashes in each column.
By including colons in the header row, you can align the text within that column:
```
| Left Aligned | Centered | Right Aligned | Left Aligned | Centered | Right Aligned |
| :----------- | :------: | ------------: | :----------- | :------: | ------------: |
| Cell 1 | Cell 2 | Cell 3 | Cell 4 | Cell 5 | Cell 6 |
| Cell 7 | Cell 8 | Cell 9 | Cell 10 | Cell 11 | Cell 12 |
```
| Left Aligned | Centered | Right Aligned | Left Aligned | Centered | Right Aligned |
| :----------- | :------: | ------------: | :----------- | :------: | ------------: |
| Cell 1 | Cell 2 | Cell 3 | Cell 4 | Cell 5 | Cell 6 |
| Cell 7 | Cell 8 | Cell 9 | Cell 10 | Cell 11 | Cell 12 |
## References
- This document leveraged heavily from the [Markdown-Cheatsheet](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet).
......
......@@ -36,3 +36,5 @@ The MemoryKiller is controlled using environment variables.
Existing jobs get 30 seconds to finish. After that, the MemoryKiller tells
Sidekiq to shut down, and an external supervision mechanism (e.g. Runit) must
restart Sidekiq.
- `SIDEKIQ_MEMORY_KILLER_SHUTDOWN_SIGNAL`: defaults to 'SIGTERM'. The name of
the final signal sent to the Sidekiq process when we want it to shut down.
......@@ -9,6 +9,8 @@ This archive will be saved in backup_path (see `config/gitlab.yml`).
The filename will be `[TIMESTAMP]_gitlab_backup.tar`. This timestamp can be used to restore an specific backup.
You can only restore a backup to exactly the same version of GitLab that you created it on, for example 7.2.1.
If you are interested in GitLab CI backup please follow to the [CI backup documentation](https://gitlab.com/gitlab-org/gitlab-ci/blob/master/doc/raketasks/backup_restore.md)*
```
# use this command if you've installed GitLab with the Omnibus package
sudo gitlab-rake gitlab:backup:create
......@@ -150,11 +152,9 @@ If you have an installation from source, please consider backing up your `gitlab
You can only restore a backup to exactly the same version of GitLab that you created it on, for example 7.2.1.
```
# Omnibus package installation
sudo gitlab-rake gitlab:backup:restore
### Installation from source
# installation from source
```
bundle exec rake gitlab:backup:restore RAILS_ENV=production
```
......@@ -196,11 +196,45 @@ Restoring repositories:
Deleting tmp directories...[DONE]
```
## Configure cron to make daily backups
### Omnibus installations
We will assume that you have installed GitLab from an omnibus package and run
`sudo gitlab-ctl reconfigure` at least once.
First make sure your backup tar file is in `/var/opt/gitlab/backups`.
```shell
sudo cp 1393513186_gitlab_backup.tar /var/opt/gitlab/backups/
```
For Omnibus package installations, see https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#scheduling-a-backup .
Next, restore the backup by running the restore command. You need to specify the
timestamp of the backup you are restoring.
For installation from source:
```shell
# Stop processes that are connected to the database
sudo gitlab-ctl stop unicorn
sudo gitlab-ctl stop sidekiq
# This command will overwrite the contents of your GitLab database!
sudo gitlab-rake gitlab:backup:restore BACKUP=1393513186
# Start GitLab
sudo gitlab-ctl start
# Create satellites
sudo gitlab-rake gitlab:satellites:create
# Check GitLab
sudo gitlab-rake gitlab:check SANITIZE=true
```
If there is a GitLab version mismatch between your backup tar file and the installed
version of GitLab, the restore command will abort with an error. Install a package for
the [required version](https://www.gitlab.com/downloads/archives/) and try again.
## Configure cron to make daily backups
### For installation from source:
```
cd /home/git/gitlab
sudo -u git -H editor config/gitlab.yml # Enable keep_time in the backup section to automatically delete old backups
......@@ -217,6 +251,32 @@ Add the following lines at the bottom:
The `CRON=1` environment setting tells the backup script to suppress all progress output if there are no errors.
This is recommended to reduce cron spam.
### For omnibus installations
To schedule a cron job that backs up your repositories and GitLab metadata, use the root user:
```
sudo su -
crontab -e
```
There, add the following line to schedule the backup for everyday at 2 AM:
```
0 2 * * * /opt/gitlab/bin/gitlab-rake gitlab:backup:create CRON=1
```
You may also want to set a limited lifetime for backups to prevent regular
backups using all your disk space. To do this add the following lines to
`/etc/gitlab/gitlab.rb` and reconfigure:
```
# limit backup lifetime to 7 days - 604800 seconds
gitlab_rails['backup_keep_time'] = 604800
```
NOTE: This cron job does not [backup your omnibus-gitlab configuration](#backup-and-restore-omnibus-gitlab-configuration) or [SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079).
## Alternative backup strategies
If your GitLab server contains a lot of Git repository data you may find the GitLab backup script to be too slow.
......
......@@ -31,3 +31,32 @@ git remote add gl git@gitlab.com:gitlab-org/gitlab-ce.git
gpa
```
# Yanking packages from packages.gitlab.com
In case something went wrong with the release and there is a need to remove the packages you can yank the packages by following the
procedure described in [package cloud documentation](https://packagecloud.io/docs#yank_pkg).
You need to have:
1. `package_cloud` gem installed (sudo gem install package_cloud)
1. Email and password for packages.gitlab.com
1. Make sure that you are supplying the url to packages.gitlab.com (default is packagecloud.io)
Example of yanking a package:
```bash
package_cloud yank --url https://packages.gitlab.com gitlab/gitlab-ce/el/6 gitlab-ce-7.10.2~omnibus-1.x86_64.rpm
```
If you are attempting this for the first time the output will look something like:
```bash
Looking for repository at gitlab/gitlab-ce... No config file exists at /Users/marin/.packagecloud. Login to create one.
Email:
marin@gitlab.com
Password:
Got your token. Writing a config file to /Users/marin/.packagecloud... success!
success!
Attempting to yank package at gitlab/gitlab-ce/el/6/gitlab-ce-7.10.2~omnibus-1.x86_64.rpm...done!
```
......@@ -29,7 +29,6 @@ All steps from issue template are explained below
```
Xth: (7 working days before the 22nd)
- [ ] Code freeze
- [ ] Update the CE changelog (#LINK)
- [ ] Update the EE changelog (#LINK)
- [ ] Update the CI changelog (#LINK)
......@@ -79,10 +78,6 @@ Xth: (1 working day before the 22nd)
- - -
## Code Freeze
Stop merging code in master, except for important bug fixes
## Update changelog
Any changes not yet added to the changelog are added by lead developer and in that merge request the complete team is
......
......@@ -44,7 +44,7 @@ Create release tag and push to remotes:
bundle exec rake release["x.x.x"]
```
### Release
## Release
1. [Build new packages with the latest version](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/release.md)
1. Apply the patch to GitLab.com and the private GitLab development server
......@@ -53,3 +53,4 @@ bundle exec rake release["x.x.x"]
1. Send tweets about the release from `@gitlab`, tweet should include the most important feature that the release is addressing and link to the blog post
1. Note in the 'GitLab X.X regressions' issue that the patch was published (CE only)
1. [Create new AMIs](https://dev.gitlab.org/gitlab/AMI/blob/master/README.md)
1. Create a new patch release issue for the next potential release
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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