mirror of
https://github.com/wahyd4/gitlabhq.git
synced 2026-08-21 10:36:04 +10:00
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
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
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
|
||||
|
||||
+2
-1
@@ -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'
|
||||
|
||||
+22
-8
@@ -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
|
||||
|
||||

|
||||
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.
|
||||
@@ -1,21 +0,0 @@
|
||||
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'
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
#= 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
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
#= 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
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
#= 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
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
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,
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
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
|
||||
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
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)
|
||||
|
||||
@@ -1,51 +1,36 @@
|
||||
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
|
||||
# Called by `TaskList::Summary`
|
||||
def task_list_items
|
||||
return [] if description.blank?
|
||||
|
||||
# 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' : ' '
|
||||
|
||||
# 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
|
||||
return '' if description.blank?
|
||||
|
||||
num_tasks = 0
|
||||
num_done = 0
|
||||
|
||||
description.scan(TASK_PATTERN_MD) do
|
||||
num_tasks += 1
|
||||
num_done += 1 unless $LAST_MATCH_INFO[:checked] == ' '
|
||||
end
|
||||
|
||||
"#{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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+17
-232
@@ -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|
|
||||
|
||||
+16
-5
@@ -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)
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
# 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
|
||||
@@ -1,7 +1,7 @@
|
||||
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
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
%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 → #{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 @@
|
||||
|
||||
.loading-gif.hide
|
||||
%p
|
||||
%i.fa.fa-spinner.fa-spin
|
||||
= icon('spinner spin')
|
||||
Saving new username
|
||||
%p.light
|
||||
= user_url(@user)
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
%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'
|
||||
@@ -0,0 +1,5 @@
|
||||
- page_title 'Recovery Codes', 'Two-factor Authentication'
|
||||
|
||||
%h3.page-title Two-factor Authentication Recovery codes
|
||||
%hr
|
||||
= render 'codes'
|
||||
@@ -0,0 +1,6 @@
|
||||
- page_title 'Two-factor Authentication', 'Account'
|
||||
|
||||
.alert.alert-success
|
||||
Congratulations! You have enabled Two-factor Authentication!
|
||||
|
||||
= render 'codes'
|
||||
@@ -0,0 +1,23 @@
|
||||
- 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'
|
||||
@@ -1,78 +1,102 @@
|
||||
.clearfix
|
||||
.append-bottom-20
|
||||
= render "shared/clone_panel"
|
||||
- unless @project.empty_repo?
|
||||
.well
|
||||
%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
|
||||
- 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, :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 Repository
|
||||
%h4
|
||||
= icon("archive fw")
|
||||
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)
|
||||
%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 namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: @ref || @repository.root_ref), class: 'btn btn-sm' do
|
||||
= 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'
|
||||
|
||||
- unless @project.empty_repo?
|
||||
.well
|
||||
%h4 Contribute
|
||||
%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
|
||||
.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
|
||||
|
||||
|
||||
= 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'
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
%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
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
%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"
|
||||
|
||||
= 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.
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
- 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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
= 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'
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
%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 & 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)
|
||||
|
||||
= 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.
|
||||
|
||||
- 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.
|
||||
|
||||
- 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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
= 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
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
.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 & 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 & 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"
|
||||
= 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
|
||||
|
||||
- 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
|
||||
|
||||
= 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
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
- 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
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
# 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
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
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
|
||||
@@ -0,0 +1,5 @@
|
||||
class AddDeviseTwoFactorBackupableToUsers < ActiveRecord::Migration
|
||||
def change
|
||||
add_column :users, :otp_backup_codes, :text
|
||||
end
|
||||
end
|
||||
+5
-1
@@ -1,7 +1,11 @@
|
||||
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
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
# 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
|
||||
@@ -0,0 +1,17 @@
|
||||
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'
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
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
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# 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
|
||||
+6
-1
@@ -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
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# 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
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
# 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
|
||||
|
||||
|
||||
+49
-44
@@ -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) ..
|
||||
# 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:
|
||||
Would generate the following link IDs:
|
||||
|
||||
###### ..Ab_c-d. e [anchor](URL) ..
|
||||
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`
|
||||
|
||||
will first be converted by step 1) into a string like:
|
||||
|
||||
```
|
||||
..Ab_c-d. e <a href="URL">anchor</a> <img src="URL" alt="alt text"/>..
|
||||
```
|
||||
|
||||
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 `` 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]
|
||||
```
|
||||
|
||||
### 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/
|
||||
```
|
||||
|
||||
Next, restore the backup by running the restore command. You need to specify the
|
||||
timestamp of the backup you are restoring.
|
||||
|
||||
```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 Omnibus package installations, see https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#scheduling-a-backup .
|
||||
|
||||
For installation from source:
|
||||
### 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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user