summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorLong Nguyen <long.polyglot@gmail.com>2016-05-21 17:21:07 +0700
committerLong Nguyen <long.polyglot@gmail.com>2016-05-21 17:21:07 +0700
commitb11cf8996ac00e58e662f9383caa113af860ce9c (patch)
tree88f47270a82cb1783a79f55ded71775385598682 /app
parentbfbbb182c14441ad176b6c7b0ec9e9f38da25ae4 (diff)
parentaa060e68ee004ac0175682afd30357a26129c0de (diff)
downloadgitlab-ce-b11cf8996ac00e58e662f9383caa113af860ce9c.tar.gz
Merge branch 'master' of https://gitlab.com/gitlab-org/gitlab-ce into issue_17479_todos_not_remove_when_leave_project
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/api.js.coffee35
-rw-r--r--app/assets/javascripts/blob/blob_gitignore_selector.js.coffee58
-rw-r--r--app/assets/javascripts/blob/edit_blob.js.coffee1
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js.coffee19
-rw-r--r--app/assets/javascripts/gl_dropdown.js.coffee82
-rw-r--r--app/assets/javascripts/issuable_form.js.coffee9
-rw-r--r--app/assets/javascripts/lib/type_utility.js.coffee9
-rw-r--r--app/assets/javascripts/merge_request_widget.js.coffee4
-rw-r--r--app/assets/stylesheets/framework/files.scss16
-rw-r--r--app/assets/stylesheets/framework/forms.scss8
-rw-r--r--app/assets/stylesheets/framework/typography.scss8
-rw-r--r--app/assets/stylesheets/framework/variables.scss1
-rw-r--r--app/assets/stylesheets/pages/editor.scss21
-rw-r--r--app/assets/stylesheets/pages/projects.scss6
-rw-r--r--app/controllers/projects/merge_requests_controller.rb3
-rw-r--r--app/controllers/projects_controller.rb9
-rw-r--r--app/controllers/registrations_controller.rb2
-rw-r--r--app/helpers/blob_helper.rb10
-rw-r--r--app/helpers/diff_helper.rb4
-rw-r--r--app/models/merge_request.rb17
-rw-r--r--app/models/milestone.rb58
-rw-r--r--app/models/repository.rb40
-rw-r--r--app/services/merge_requests/create_service.rb7
-rw-r--r--app/services/merge_requests/merge_service.rb8
-rw-r--r--app/services/merge_requests/update_service.rb2
-rw-r--r--app/services/projects/autocomplete_service.rb4
-rw-r--r--app/services/system_note_service.rb9
-rw-r--r--app/views/projects/blob/_editor.html.haml3
-rw-r--r--app/views/projects/buttons/_fork.html.haml5
-rw-r--r--app/views/projects/diffs/_file.html.haml6
-rw-r--r--app/views/projects/empty.html.haml10
-rw-r--r--app/views/projects/merge_requests/widget/open/_accept.html.haml5
-rw-r--r--app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml5
-rw-r--r--app/views/projects/merge_requests/widget/open/_not_allowed.html.haml4
-rw-r--r--app/views/shared/groups/_list.html.haml2
-rw-r--r--app/views/shared/issuable/_form.html.haml89
36 files changed, 436 insertions, 143 deletions
diff --git a/app/assets/javascripts/api.js.coffee b/app/assets/javascripts/api.js.coffee
index dd1bbb37551..3f61ea1eaf4 100644
--- a/app/assets/javascripts/api.js.coffee
+++ b/app/assets/javascripts/api.js.coffee
@@ -1,14 +1,15 @@
@Api =
- groups_path: "/api/:version/groups.json"
- group_path: "/api/:version/groups/:id.json"
- namespaces_path: "/api/:version/namespaces.json"
- group_projects_path: "/api/:version/groups/:id/projects.json"
- projects_path: "/api/:version/projects.json"
- labels_path: "/api/:version/projects/:id/labels"
- license_path: "/api/:version/licenses/:key"
+ groupsPath: "/api/:version/groups.json"
+ groupPath: "/api/:version/groups/:id.json"
+ namespacesPath: "/api/:version/namespaces.json"
+ groupProjectsPath: "/api/:version/groups/:id/projects.json"
+ projectsPath: "/api/:version/projects.json"
+ labelsPath: "/api/:version/projects/:id/labels"
+ licensePath: "/api/:version/licenses/:key"
+ gitignorePath: "/api/:version/gitignores/:key"
group: (group_id, callback) ->
- url = Api.buildUrl(Api.group_path)
+ url = Api.buildUrl(Api.groupPath)
url = url.replace(':id', group_id)
$.ajax(
@@ -22,7 +23,7 @@
# Return groups list. Filtered by query
# Only active groups retrieved
groups: (query, skip_ldap, callback) ->
- url = Api.buildUrl(Api.groups_path)
+ url = Api.buildUrl(Api.groupsPath)
$.ajax(
url: url
@@ -36,7 +37,7 @@
# Return namespaces list. Filtered by query
namespaces: (query, callback) ->
- url = Api.buildUrl(Api.namespaces_path)
+ url = Api.buildUrl(Api.namespacesPath)
$.ajax(
url: url
@@ -50,7 +51,7 @@
# Return projects list. Filtered by query
projects: (query, order, callback) ->
- url = Api.buildUrl(Api.projects_path)
+ url = Api.buildUrl(Api.projectsPath)
$.ajax(
url: url
@@ -64,7 +65,7 @@
callback(projects)
newLabel: (project_id, data, callback) ->
- url = Api.buildUrl(Api.labels_path)
+ url = Api.buildUrl(Api.labelsPath)
url = url.replace(':id', project_id)
data.private_token = gon.api_token
@@ -80,7 +81,7 @@
# Return group projects list. Filtered by query
groupProjects: (group_id, query, callback) ->
- url = Api.buildUrl(Api.group_projects_path)
+ url = Api.buildUrl(Api.groupProjectsPath)
url = url.replace(':id', group_id)
$.ajax(
@@ -95,7 +96,7 @@
# Return text for a specific license
licenseText: (key, data, callback) ->
- url = Api.buildUrl(Api.license_path).replace(':key', key)
+ url = Api.buildUrl(Api.licensePath).replace(':key', key)
$.ajax(
url: url
@@ -103,6 +104,12 @@
).done (license) ->
callback(license)
+ gitignoreText: (key, callback) ->
+ url = Api.buildUrl(Api.gitignorePath).replace(':key', key)
+
+ $.get url, (gitignore) ->
+ callback(gitignore)
+
buildUrl: (url) ->
url = gon.relative_url_root + url if gon.relative_url_root?
return url.replace(':version', gon.api_version)
diff --git a/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee b/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee
new file mode 100644
index 00000000000..cc8a497d081
--- /dev/null
+++ b/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee
@@ -0,0 +1,58 @@
+class @BlobGitignoreSelector
+ constructor: (opts) ->
+ {
+ @dropdown
+ @editor
+ @$wrapper = @dropdown.closest('.gitignore-selector')
+ @$filenameInput = $('#file_name')
+ @data = @dropdown.data('filenames')
+ } = opts
+
+ @dropdown.glDropdown(
+ data: @data,
+ filterable: true,
+ selectable: true,
+ search:
+ fields: ['name']
+ clicked: @onClick
+ text: (gitignore) ->
+ gitignore.name
+ )
+
+ @toggleGitignoreSelector()
+ @bindEvents()
+
+ bindEvents: ->
+ @$filenameInput
+ .on 'keyup blur', (e) =>
+ @toggleGitignoreSelector()
+
+ toggleGitignoreSelector: ->
+ filename = @$filenameInput.val() or $('.editor-file-name').text().trim()
+ @$wrapper.toggleClass 'hidden', filename isnt '.gitignore'
+
+ onClick: (item, el, e) =>
+ e.preventDefault()
+ @requestIgnoreFile(item.name)
+
+ requestIgnoreFile: (name) ->
+ Api.gitignoreText name, @requestIgnoreFileSuccess.bind(@)
+
+ requestIgnoreFileSuccess: (gitignore) ->
+ @editor.setValue(gitignore.content, 1)
+ @editor.focus()
+
+class @BlobGitignoreSelectors
+ constructor: (opts) ->
+ {
+ @$dropdowns = $('.js-gitignore-selector')
+ @editor
+ } = opts
+
+ @$dropdowns.each (i, dropdown) =>
+ $dropdown = $(dropdown)
+
+ new BlobGitignoreSelector(
+ dropdown: $dropdown,
+ editor: @editor
+ )
diff --git a/app/assets/javascripts/blob/edit_blob.js.coffee b/app/assets/javascripts/blob/edit_blob.js.coffee
index eea9aa972ee..79141e768b8 100644
--- a/app/assets/javascripts/blob/edit_blob.js.coffee
+++ b/app/assets/javascripts/blob/edit_blob.js.coffee
@@ -13,6 +13,7 @@ class @EditBlob
@initModePanesAndLinks()
new BlobLicenseSelector(@editor)
+ new BlobGitignoreSelectors(editor: @editor)
initModePanesAndLinks: ->
@$editModePanes = $(".js-edit-mode-pane")
diff --git a/app/assets/javascripts/gfm_auto_complete.js.coffee b/app/assets/javascripts/gfm_auto_complete.js.coffee
index 61e3f811e73..41dba342107 100644
--- a/app/assets/javascripts/gfm_auto_complete.js.coffee
+++ b/app/assets/javascripts/gfm_auto_complete.js.coffee
@@ -18,6 +18,10 @@ GitLab.GfmAutoComplete =
Issues:
template: '<li><small>${id}</small> ${title}</li>'
+ # Milestones
+ Milestones:
+ template: '<li>${title}</li>'
+
# Add GFM auto-completion to all input fields, that accept GFM input.
setup: (wrap) ->
@input = $('.js-gfm-input')
@@ -82,6 +86,19 @@ GitLab.GfmAutoComplete =
search: "#{i.iid} #{i.title}"
@input.atwho
+ at: '%'
+ alias: 'milestones'
+ searchKey: 'search'
+ displayTpl: @Milestones.template
+ insertTpl: '${atwho-at}"${title}"'
+ callbacks:
+ beforeSave: (milestones) ->
+ $.map milestones, (m) ->
+ id: m.iid
+ title: sanitize(m.title)
+ search: "#{m.title}"
+
+ @input.atwho
at: '!'
alias: 'mergerequests'
searchKey: 'search'
@@ -105,6 +122,8 @@ GitLab.GfmAutoComplete =
@input.atwho 'load', '@', data.members
# load issues
@input.atwho 'load', 'issues', data.issues
+ # load milestones
+ @input.atwho 'load', 'milestones', data.milestones
# load merge requests
@input.atwho 'load', 'mergerequests', data.mergerequests
# load emojis
diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee
index 1d1bfeb2e77..b3f1dc969b8 100644
--- a/app/assets/javascripts/gl_dropdown.js.coffee
+++ b/app/assets/javascripts/gl_dropdown.js.coffee
@@ -60,9 +60,36 @@ class GitLabDropdownFilter
results = data
if search_text isnt ''
- results = fuzzaldrinPlus.filter(data, search_text,
- key: @options.keys
- )
+ # When data is an array of objects therefore [object Array] e.g.
+ # [
+ # { prop: 'foo' },
+ # { prop: 'baz' }
+ # ]
+ if _.isArray(data)
+ results = fuzzaldrinPlus.filter(data, search_text,
+ key: @options.keys
+ )
+ else
+ # If data is grouped therefore an [object Object]. e.g.
+ # {
+ # groupName1: [
+ # { prop: 'foo' },
+ # { prop: 'baz' }
+ # ],
+ # groupName2: [
+ # { prop: 'abc' },
+ # { prop: 'def' }
+ # ]
+ # }
+ if gl.utils.isObject data
+ results = {}
+ for key, group of data
+ tmp = fuzzaldrinPlus.filter(group, search_text,
+ key: @options.keys
+ )
+
+ if tmp.length
+ results[key] = tmp.map (item) -> item
@options.callback results
else
@@ -141,8 +168,9 @@ class GitLabDropdown
searchFields = if @options.search then @options.search.fields else [];
if @options.data
- # If data is an array
- if _.isArray @options.data
+ # If we provided data
+ # data could be an array of objects or a group of arrays
+ if _.isObject(@options.data) and not _.isFunction(@options.data)
@fullData = @options.data
@parseData @options.data
else
@@ -230,19 +258,33 @@ class GitLabDropdown
parseData: (data) ->
@renderedData = data
- # Render each row
- html = $.map data, (obj) =>
- return @renderItem(obj)
-
if @options.filterable and data.length is 0
# render no matching results
html = [@noResults()]
+ else
+ # Handle array groups
+ if gl.utils.isObject data
+ html = []
+ for name, groupData of data
+ # Add header for each group
+ html.push(@renderItem(header: name, name))
+
+ @renderData(groupData, name)
+ .map (item) ->
+ html.push item
+ else
+ # Render each row
+ html = @renderData(data)
# Render the full menu
full_html = @renderMenu(html.join(""))
@appendMenu(full_html)
+ renderData: (data, group = false) ->
+ data.map (obj, index) =>
+ return @renderItem(obj, group, index)
+
shouldPropagate: (e) =>
if @options.multiSelect
$target = $(e.target)
@@ -299,11 +341,10 @@ class GitLabDropdown
selector = '.dropdown-content'
if @dropdown.find(".dropdown-toggle-page").length
selector = ".dropdown-page-one .dropdown-content"
-
$(selector, @dropdown).html html
# Render the row
- renderItem: (data) ->
+ renderItem: (data, group = false, index = false) ->
html = ""
# Divider
@@ -346,8 +387,13 @@ class GitLabDropdown
if @highlight
text = @highlightTextMatches(text, @filterInput.val())
+ if group
+ groupAttrs = "data-group='#{group}' data-index='#{index}'"
+ else
+ groupAttrs = ''
+
html = "<li>
- <a href='#{url}' class='#{cssClass}'>
+ <a href='#{url}' #{groupAttrs} class='#{cssClass}'>
#{text}
</a>
</li>"
@@ -377,9 +423,15 @@ class GitLabDropdown
rowClicked: (el) ->
fieldName = @options.fieldName
- selectedIndex = el.parent().index()
if @renderedData
- selectedObject = @renderedData[selectedIndex]
+ groupName = el.data('group')
+ if groupName
+ selectedIndex = el.data('index')
+ selectedObject = @renderedData[groupName][selectedIndex]
+ else
+ selectedIndex = el.closest('li').index()
+ selectedObject = @renderedData[selectedIndex]
+
value = if @options.id then @options.id(selectedObject, el) else selectedObject.id
field = @dropdown.parent().find("input[name='#{fieldName}'][value='#{value}']")
if el.hasClass(ACTIVE_CLASS)
@@ -460,7 +512,7 @@ class GitLabDropdown
return false
if currentKeyCode is 13
- @selectRowAtIndex currentIndex
+ @selectRowAtIndex if currentIndex < 0 then 0 else currentIndex
removeArrayKeyEvent: ->
$('body').off 'keydown'
diff --git a/app/assets/javascripts/issuable_form.js.coffee b/app/assets/javascripts/issuable_form.js.coffee
index 7a788f761b7..72ae3bde81e 100644
--- a/app/assets/javascripts/issuable_form.js.coffee
+++ b/app/assets/javascripts/issuable_form.js.coffee
@@ -20,6 +20,15 @@ class @IssuableForm
@initWip()
+ $issuableDueDate = $('#issuable-due-date')
+
+ if $issuableDueDate.length
+ $('.datepicker').datepicker(
+ dateFormat: 'yy-mm-dd',
+ onSelect: (dateText, inst) ->
+ $issuableDueDate.val dateText
+ ).datepicker 'setDate', $.datepicker.parseDate('yy-mm-dd', $issuableDueDate.val())
+
initAutosave: ->
new Autosave @titleField, [
document.location.pathname,
diff --git a/app/assets/javascripts/lib/type_utility.js.coffee b/app/assets/javascripts/lib/type_utility.js.coffee
new file mode 100644
index 00000000000..957f0d86b36
--- /dev/null
+++ b/app/assets/javascripts/lib/type_utility.js.coffee
@@ -0,0 +1,9 @@
+((w) ->
+
+ w.gl ?= {}
+ w.gl.utils ?= {}
+
+ w.gl.utils.isObject = (obj) ->
+ obj? and (obj.constructor is Object)
+
+) window
diff --git a/app/assets/javascripts/merge_request_widget.js.coffee b/app/assets/javascripts/merge_request_widget.js.coffee
index f58647988a2..b6d590f681c 100644
--- a/app/assets/javascripts/merge_request_widget.js.coffee
+++ b/app/assets/javascripts/merge_request_widget.js.coffee
@@ -113,7 +113,7 @@ class @MergeRequestWidget
switch state
when "failed", "canceled", "not_found"
@setMergeButtonClass('btn-danger')
- when "running", "pending"
+ when "running"
@setMergeButtonClass('btn-warning')
when "success"
@setMergeButtonClass('btn-create')
@@ -126,6 +126,6 @@ class @MergeRequestWidget
$('.ci_widget:visible .ci-coverage').text(text)
setMergeButtonClass: (css_class) ->
- $('.accept_merge_request')
+ $('.js-merge-button')
.removeClass('btn-danger btn-warning btn-create')
.addClass(css_class)
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 61d9954c6c8..8c96c7a9c31 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -36,22 +36,6 @@
}
}
- .filename {
- &.old {
- display: inline-block;
- span.idiff {
- background-color: #f8cbcb;
- }
- }
-
- &.new {
- display: inline-block;
- span.idiff {
- background-color: #a6f3a6;
- }
- }
- }
-
a:not(.btn) {
color: $gl-dark-link-color;
}
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index 558b133f593..46acc3b772f 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -28,10 +28,6 @@ input[type='text'].danger {
}
label {
- &.control-label {
- @extend .col-sm-2;
- }
-
&.inline-label {
margin: 0;
}
@@ -41,6 +37,10 @@ label {
}
}
+.control-label {
+ @extend .col-sm-2;
+}
+
.inline-input-group {
width: 250px;
}
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 2779cd56788..3575984b229 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -269,3 +269,11 @@ h1, h2, h3, h4 {
text-align: right;
}
}
+
+.idiff.deletion {
+ background: $line-removed-dark;
+}
+
+.idiff.addition {
+ background: $line-added-dark;
+}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 7360bdbcc82..c5a4dbe372c 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -178,6 +178,7 @@ $table-border-gray: #f0f0f0;
$line-target-blue: #eaf3fc;
$line-select-yellow: #fcf8e7;
$line-select-yellow-dark: #f0e2bd;
+
/*
* Fonts
*/
diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss
index 8981f070a20..22679c764dc 100644
--- a/app/assets/stylesheets/pages/editor.scss
+++ b/app/assets/stylesheets/pages/editor.scss
@@ -23,7 +23,7 @@
.file-title {
@extend .monospace;
- line-height: 42px;
+ line-height: 35px;
padding-top: 7px;
padding-bottom: 7px;
@@ -43,7 +43,7 @@
.editor-file-name {
@extend .monospace;
-
+
float: left;
margin-right: 10px;
}
@@ -59,7 +59,22 @@
}
.encoding-selector,
- .license-selector {
+ .license-selector,
+ .gitignore-selector {
display: inline-block;
+ vertical-align: top;
+ font-family: $regular_font;
+ }
+
+ .gitignore-selector {
+
+ .dropdown {
+ line-height: 21px;
+ }
+
+ .dropdown-menu-toggle {
+ vertical-align: top;
+ width: 220px;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index a3690e40e28..b4702b31182 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -149,6 +149,10 @@
white-space: nowrap;
margin: 0 11px 0 4px;
+ a {
+ color: inherit;
+ }
+
&:hover {
background: #fff;
}
@@ -161,7 +165,7 @@
display: inline-table;
margin-right: 12px;
- a {
+ > a {
margin: -1px;
}
}
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index c5757a24624..f137c12d215 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -334,7 +334,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
params.require(:merge_request).permit(
:title, :assignee_id, :source_project_id, :source_branch,
:target_project_id, :target_branch, :milestone_id,
- :state_event, :description, :task_num, label_ids: []
+ :state_event, :description, :task_num, :force_remove_source_branch,
+ label_ids: []
)
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index f4ec60ad2c7..9697b88c032 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -101,13 +101,7 @@ class ProjectsController < Projects::ApplicationController
respond_to do |format|
format.html do
- if current_user
- @membership = @project.team.find_member(current_user.id)
-
- if @membership
- @notification_setting = current_user.notification_settings_for(@project)
- end
- end
+ @notification_setting = current_user.notification_settings_for(@project) if current_user
if @project.repository_exists?
if @project.empty_repo?
@@ -147,6 +141,7 @@ class ProjectsController < Projects::ApplicationController
@suggestions = {
emojis: AwardEmoji.urls,
issues: autocomplete.issues,
+ milestones: autocomplete.milestones,
mergerequests: autocomplete.merge_requests,
members: participants
}
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index 26eb15f49e4..75b78a49eab 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -38,7 +38,7 @@ class RegistrationsController < Devise::RegistrationsController
end
def after_sign_up_path_for(user)
- user.confirmed_at.present? ? dashboard_projects_path : users_almost_there_path
+ user.confirmed? ? dashboard_projects_path : users_almost_there_path
end
def after_inactive_sign_up_path_for(_resource)
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 93241b3afb7..cec2dc753fe 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -184,4 +184,14 @@ module BlobHelper
Other: licenses.reject(&:featured).map { |license| [license.name, license.key] }
}
end
+
+ def gitignore_names
+ return @gitignore_names if defined?(@gitignore_names)
+
+ @gitignore_names = {
+ Global: Gitlab::Gitignore.global.map { |gitignore| { name: gitignore.name } },
+ # Note that the key here doesn't cover it really
+ Languages: Gitlab::Gitignore.languages_frameworks.map{ |gitignore| { name: gitignore.name } }
+ }
+ end
end
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index 5f311f3780a..ea383f9b0f6 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -2,8 +2,8 @@ module DiffHelper
def mark_inline_diffs(old_line, new_line)
old_diffs, new_diffs = Gitlab::Diff::InlineDiff.new(old_line, new_line).inline_diffs
- marked_old_line = Gitlab::Diff::InlineDiffMarker.new(old_line).mark(old_diffs)
- marked_new_line = Gitlab::Diff::InlineDiffMarker.new(new_line).mark(new_diffs)
+ marked_old_line = Gitlab::Diff::InlineDiffMarker.new(old_line).mark(old_diffs, mode: :deletion)
+ marked_new_line = Gitlab::Diff::InlineDiffMarker.new(new_line).mark(new_diffs, mode: :addition)
[marked_old_line, marked_new_line]
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 45ddcf6812a..722c258244c 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -286,6 +286,18 @@ class MergeRequest < ActiveRecord::Base
last_commit == source_project.commit(source_branch)
end
+ def should_remove_source_branch?
+ merge_params['should_remove_source_branch'].present?
+ end
+
+ def force_remove_source_branch?
+ merge_params['force_remove_source_branch'].present?
+ end
+
+ def remove_source_branch?
+ should_remove_source_branch? || force_remove_source_branch?
+ end
+
def mr_and_commit_notes
# Fetch comments only from last 100 commits
commits_for_notes_limit = 100
@@ -426,7 +438,10 @@ class MergeRequest < ActiveRecord::Base
self.merge_when_build_succeeds = false
self.merge_user = nil
- self.merge_params = nil
+ if merge_params
+ merge_params.delete('should_remove_source_branch')
+ merge_params.delete('commit_message')
+ end
self.save
end
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index fe9a281f366..e0c8454a998 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -59,8 +59,27 @@ class Milestone < ActiveRecord::Base
end
end
+ def self.reference_prefix
+ '%'
+ end
+
def self.reference_pattern
- nil
+ # NOTE: The iid pattern only matches when all characters on the expression
+ # are digits, so it will match %2 but not %2.1 because that's probably a
+ # milestone name and we want it to be matched as such.
+ @reference_pattern ||= %r{
+ (#{Project.reference_pattern})?
+ #{Regexp.escape(reference_prefix)}
+ (?:
+ (?<milestone_iid>
+ \d+(?!\S\w)\b # Integer-based milestone iid, or
+ ) |
+ (?<milestone_name>
+ [^"\s]+\b | # String-based single-word milestone title, or
+ "[^"]+" # String-based multi-word milestone surrounded in quotes
+ )
+ )
+ }x
end
def self.link_reference_pattern
@@ -81,13 +100,26 @@ class Milestone < ActiveRecord::Base
end
end
- def to_reference(from_project = nil)
- escaped_title = self.title.gsub("]", "\\]")
-
- h = Gitlab::Routing.url_helpers
- url = h.namespace_project_milestone_url(self.project.namespace, self.project, self)
+ ##
+ # Returns the String necessary to reference this Milestone in Markdown
+ #
+ # format - Symbol format to use (default: :iid, optional: :name)
+ #
+ # Examples:
+ #
+ # Milestone.first.to_reference # => "%1"
+ # Milestone.first.to_reference(format: :name) # => "%\"goal\""
+ # Milestone.first.to_reference(project) # => "gitlab-org/gitlab-ce%1"
+ #
+ def to_reference(from_project = nil, format: :iid)
+ format_reference = milestone_format_reference(format)
+ reference = "#{self.class.reference_prefix}#{format_reference}"
- "[#{escaped_title}](#{url})"
+ if cross_project_reference?(from_project)
+ project.to_reference + reference
+ else
+ reference
+ end
end
def reference_link_text(from_project = nil)
@@ -159,4 +191,16 @@ class Milestone < ActiveRecord::Base
issues.where(id: ids).
update_all(["position = CASE #{conditions} ELSE position END", *pairs])
end
+
+ private
+
+ def milestone_format_reference(format = :iid)
+ raise ArgumentError, 'Unknown format' unless [:iid, :name].include?(format)
+
+ if format == :name && !name.include?('"')
+ %("#{name}")
+ else
+ iid
+ end
+ end
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 3716ea6ad6c..ecc8795c954 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -245,7 +245,7 @@ class Repository
def cache_keys
%i(size branch_names tag_names commit_count
readme version contribution_guide changelog
- license_blob license_key)
+ license_blob license_key gitignore)
end
def build_cache
@@ -256,6 +256,10 @@ class Repository
end
end
+ def expire_gitignore
+ cache.expire(:gitignore)
+ end
+
def expire_tags_cache
cache.expire(:tag_names)
@tags = nil
@@ -472,33 +476,37 @@ class Repository
def changelog
cache.fetch(:changelog) do
- tree(:head).blobs.find do |file|
- file.name =~ /\A(changelog|history|changes|news)/i
- end
+ file_on_head(/\A(changelog|history|changes|news)/i)
end
end
def license_blob
- return nil if !exists? || empty?
+ return nil unless head_exists?
cache.fetch(:license_blob) do
- tree(:head).blobs.find do |file|
- file.name =~ /\A(licen[sc]e|copying)(\..+|\z)/i
- end
+ file_on_head(/\A(licen[sc]e|copying)(\..+|\z)/i)
end
end
def license_key
- return nil if !exists? || empty?
+ return nil unless head_exists?
cache.fetch(:license_key) do
Licensee.license(path).try(:key)
end
end
- def gitlab_ci_yml
+ def gitignore
return nil if !exists? || empty?
+ cache.fetch(:gitignore) do
+ file_on_head(/\A\.gitignore\z/)
+ end
+ end
+
+ def gitlab_ci_yml
+ return nil unless head_exists?
+
@gitlab_ci_yml ||= tree(:head).blobs.find do |file|
file.name == '.gitlab-ci.yml'
end
@@ -854,7 +862,7 @@ class Repository
def search_files(query, ref)
offset = 2
- args = %W(#{Gitlab.config.git.bin_path} grep -i -I -n --before-context #{offset} --after-context #{offset} -e #{Regexp.escape(query)} #{ref || root_ref})
+ args = %W(#{Gitlab.config.git.bin_path} grep -i -I -n --before-context #{offset} --after-context #{offset} -E -e #{Regexp.escape(query)} #{ref || root_ref})
Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/)
end
@@ -965,7 +973,7 @@ class Repository
end
def main_language
- return if empty? || rugged.head_unborn?
+ return unless head_exists?
Linguist::Repository.new(rugged, rugged.head.target_id).language
end
@@ -985,4 +993,12 @@ class Repository
def cache
@cache ||= RepositoryCache.new(path_with_namespace)
end
+
+ def head_exists?
+ exists? && !empty? && !rugged.head_unborn?
+ end
+
+ def file_on_head(regex)
+ tree(:head).blobs.find { |file| file.name =~ regex }
+ end
end
diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb
index 33609d01f20..96a25330af1 100644
--- a/app/services/merge_requests/create_service.rb
+++ b/app/services/merge_requests/create_service.rb
@@ -8,11 +8,14 @@ module MergeRequests
@project = Project.find(params[:target_project_id]) if params[:target_project_id]
filter_params
- label_params = params[:label_ids]
- merge_request = MergeRequest.new(params.except(:label_ids))
+ label_params = params.delete(:label_ids)
+ force_remove_source_branch = params.delete(:force_remove_source_branch)
+
+ merge_request = MergeRequest.new(params)
merge_request.source_project = source_project
merge_request.target_project ||= source_project
merge_request.author = current_user
+ merge_request.merge_params['force_remove_source_branch'] = force_remove_source_branch
if merge_request.save
merge_request.update_attributes(label_ids: label_params)
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index 9a58383b398..9aaf5a5e561 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -45,10 +45,14 @@ module MergeRequests
def after_merge
MergeRequests::PostMergeService.new(project, current_user).execute(merge_request)
- if params[:should_remove_source_branch].present?
- DeleteBranchService.new(@merge_request.source_project, current_user).
+ if params[:should_remove_source_branch].present? || @merge_request.force_remove_source_branch?
+ DeleteBranchService.new(@merge_request.source_project, branch_deletion_user).
execute(merge_request.source_branch)
end
end
+
+ def branch_deletion_user
+ @merge_request.force_remove_source_branch? ? @merge_request.author : current_user
+ end
end
end
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 477c64e7377..026a37997d4 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -11,6 +11,8 @@ module MergeRequests
params.except!(:target_project_id)
params.except!(:source_branch)
+ merge_request.merge_params['force_remove_source_branch'] = params.delete(:force_remove_source_branch)
+
update(merge_request)
end
diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb
index ba50305dbd5..eb73948006e 100644
--- a/app/services/projects/autocomplete_service.rb
+++ b/app/services/projects/autocomplete_service.rb
@@ -4,6 +4,10 @@ module Projects
@project.issues.visible_to_user(current_user).opened.select([:iid, :title])
end
+ def milestones
+ @project.milestones.active.reorder(due_date: :asc, title: :asc).select([:iid, :title])
+ end
+
def merge_requests
@project.merge_requests.opened.select([:iid, :title])
end
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 972f8b2012d..4e8fa0818b9 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -169,7 +169,14 @@ class SystemNoteService
#
# Returns the created Note object
def self.change_title(noteable, project, author, old_title)
- body = "Title changed from **#{old_title}** to **#{noteable.title}**"
+ new_title = noteable.title.dup
+
+ old_diffs, new_diffs = Gitlab::Diff::InlineDiff.new(old_title, new_title).inline_diffs
+
+ marked_old_title = Gitlab::Diff::InlineDiffMarker.new(old_title).mark(old_diffs, mode: :deletion, markdown: true)
+ marked_new_title = Gitlab::Diff::InlineDiffMarker.new(new_title).mark(new_diffs, mode: :addition, markdown: true)
+
+ body = "Changed title: **#{marked_old_title}** → **#{marked_new_title}**"
create_note(noteable: noteable, project: project, author: author, note: body)
end
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index fefa652a3da..4071b59c003 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -16,6 +16,9 @@
.license-selector.js-license-selector.hide
= select_tag :license_type, grouped_options_for_select(licenses_for_select, @project.repository.license_key), include_blank: true, class: 'select2 license-select', data: {placeholder: 'Choose a license template', project: @project.name, fullname: @project.namespace.human_name}
+ .gitignore-selector.hidden
+ = dropdown_tag("Choose a .gitignore template", options: { toggle_class: 'js-gitignore-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { filenames: gitignore_names } } )
+
.encoding-selector
= select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2'
diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml
index 5fb5fe5af2f..34ad9fe2c43 100644
--- a/app/views/projects/buttons/_fork.html.haml
+++ b/app/views/projects/buttons/_fork.html.haml
@@ -12,7 +12,8 @@
= link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn has-tooltip' do
= icon('code-fork fw')
Fork
- = link_to namespace_project_forks_path(@project.namespace, @project), class: 'count-with-arrow' do
+ %div.count-with-arrow
%span.arrow
%span.count
- = @project.forks_count
+ = link_to namespace_project_forks_path(@project.namespace, @project) do
+ = @project.forks_count
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index f10b5094eaf..e5983c58039 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -11,11 +11,9 @@
= link_to "#diff-#{i}" do
- if diff_file.renamed_file
- old_path, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
- .filename.old
- = old_path
+ = old_path
&rarr;
- .filename.new
- = new_path
+ = new_path
- else
%span
= diff_file.new_path
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index 1a2e59752fe..636beb73ec2 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -15,10 +15,14 @@
If you already have files you can push them using command line instructions below.
%p
Otherwise you can start with adding a
- = link_to "README", new_readme_path, class: 'underlined-link'
+ = succeed ',' do
+ = link_to "README", new_readme_path, class: 'underlined-link'
+ a
+ = succeed ',' do
+ = link_to "LICENSE", add_special_file_path(@project, file_name: 'LICENSE'), class: 'underlined-link'
or a
- = link_to "LICENSE", add_special_file_path(@project, file_name: 'LICENSE'), class: 'underlined-link'
- file to this project.
+ = link_to '.gitignore', add_special_file_path(@project, file_name: '.gitignore'), class: 'underlined-link'
+ to this project.
- if can?(current_user, :push_code, @project)
%div{ class: container_class }
diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml
index 807833741af..cfdf4edac37 100644
--- a/app/views/projects/merge_requests/widget/open/_accept.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml
@@ -25,7 +25,10 @@
- else
= f.button class: "btn btn-create btn-grouped js-merge-button accept_merge_request #{status_class}" do
Accept Merge Request
- - if @merge_request.can_remove_source_branch?(current_user)
+ - if @merge_request.force_remove_source_branch?
+ .accept-control
+ The source branch will be removed.
+ - elsif @merge_request.can_remove_source_branch?(current_user)
.accept-control.checkbox
= label_tag :should_remove_source_branch, class: "remove_source_checkbox" do
= check_box_tag :should_remove_source_branch
diff --git a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml
index 2168294c683..b83ddcab3a4 100644
--- a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml
@@ -2,17 +2,16 @@
Set by #{link_to_member(@project, @merge_request.merge_user, avatar: true)}
to be merged automatically when the build succeeds.
%div
- - should_remove_source_branch = @merge_request.merge_params["should_remove_source_branch"].present?
%p
= succeed '.' do
The changes will be merged into
%span.label-branch= @merge_request.target_branch
- - if should_remove_source_branch
+ - if @merge_request.remove_source_branch?
The source branch will be removed.
- else
The source branch will not be removed.
- - remove_source_branch_button = @merge_request.can_remove_source_branch?(current_user) && !should_remove_source_branch && @merge_request.merge_user == current_user
+ - remove_source_branch_button = !@merge_request.remove_source_branch? && @merge_request.can_remove_source_branch?(current_user) && @merge_request.merge_user == current_user
- user_can_cancel_automatic_merge = @merge_request.can_cancel_merge_when_build_succeeds?(current_user)
- if remove_source_branch_button || user_can_cancel_automatic_merge
.clearfix.prepend-top-10
diff --git a/app/views/projects/merge_requests/widget/open/_not_allowed.html.haml b/app/views/projects/merge_requests/widget/open/_not_allowed.html.haml
index a8145558ca8..57ce1959021 100644
--- a/app/views/projects/merge_requests/widget/open/_not_allowed.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_not_allowed.html.haml
@@ -1,4 +1,6 @@
-%h4
+%h4
Ready to be merged automatically
%p
Ask someone with write access to this repository to merge this request.
+ - if @merge_request.force_remove_source_branch?
+ The source branch will be removed.
diff --git a/app/views/shared/groups/_list.html.haml b/app/views/shared/groups/_list.html.haml
index 1aa7ed1f2eb..427595c47a5 100644
--- a/app/views/shared/groups/_list.html.haml
+++ b/app/views/shared/groups/_list.html.haml
@@ -3,4 +3,4 @@
- groups.each_with_index do |group, i|
= render "shared/groups/group", group: group
- else
- %h3 No groups found
+ .nothing-here-block No groups found
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index 5c52cc6d1da..fc3410f425d 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -44,45 +44,53 @@
This issue is confidential and should only be visible to team members
- if can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project)
+ - has_due_date = issuable.has_attribute?(:due_date)
%hr
- .form-group
- .issue-assignee
- = f.label :assignee_id, "Assignee", class: 'control-label'
- .col-sm-10
- .issuable-form-select-holder
- = users_select_tag("#{issuable.class.model_name.param_key}[assignee_id]",
- placeholder: 'Select assignee', class: 'custom-form-control', null_user: true,
- selected: issuable.assignee_id, project: @target_project || @project,
- first_user: true, current_user: true, include_blank: true)
- &nbsp;
- = link_to 'Assign to me', '#', class: 'btn assign-to-me-link'
- .form-group
- .issue-milestone
- = f.label :milestone_id, "Milestone", class: 'control-label'
- .col-sm-10
- - if milestone_options(issuable).present?
+ .row
+ %div{ class: (has_due_date ? "col-lg-6" : "col-sm-12") }
+ .form-group.issue-assignee
+ = f.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}"
+ .col-sm-10{ class: ("col-lg-8" if has_due_date) }
.issuable-form-select-holder
- = f.select(:milestone_id, milestone_options(issuable),
- { include_blank: true }, { class: 'select2', data: { placeholder: 'Select milestone' } })
- - else
- .prepend-top-10
- %span.light No open milestones available.
- &nbsp;
- - if can? current_user, :admin_milestone, issuable.project
- = link_to 'Create new milestone', new_namespace_project_milestone_path(issuable.project.namespace, issuable.project), target: :blank
- .form-group
- - has_labels = issuable.project.labels.any?
- = f.label :label_ids, "Labels", class: 'control-label'
- .col-sm-10{ class: ('issuable-form-padding-top' if !has_labels) }
- - if has_labels
- .issuable-form-select-holder
- = f.collection_select :label_ids, issuable.project.labels.all, :id, :name,
- { selected: issuable.label_ids }, multiple: true, class: 'select2', data: { placeholder: "Select labels" }
- - else
- %span.light No labels yet.
- &nbsp;
- - if can? current_user, :admin_label, issuable.project
- = link_to 'Create new label', new_namespace_project_label_path(issuable.project.namespace, issuable.project), target: :blank
+ = users_select_tag("#{issuable.class.model_name.param_key}[assignee_id]",
+ placeholder: 'Select assignee', class: 'custom-form-control', null_user: true,
+ selected: issuable.assignee_id, project: @target_project || @project,
+ first_user: true, current_user: true, include_blank: true)
+ %div
+ = link_to 'Assign to me', '#', class: 'assign-to-me-link prepend-top-5 inline'
+ .form-group.issue-milestone
+ = f.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}"
+ .col-sm-10{ class: ("col-lg-8" if has_due_date) }
+ - if milestone_options(issuable).present?
+ .issuable-form-select-holder
+ = f.select(:milestone_id, milestone_options(issuable),
+ { include_blank: true }, { class: 'select2', data: { placeholder: 'Select milestone' } })
+ - else
+ .prepend-top-10
+ %span.light No open milestones available.
+ - if can? current_user, :admin_milestone, issuable.project
+ %div
+ = link_to 'Create new milestone', new_namespace_project_milestone_path(issuable.project.namespace, issuable.project), target: :blank, class: "prepend-top-5 inline"
+ .form-group
+ - has_labels = issuable.project.labels.any?
+ = f.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}"
+ .col-sm-10{ class: "#{"col-lg-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" }
+ - if has_labels
+ .issuable-form-select-holder
+ = f.collection_select :label_ids, issuable.project.labels.all, :id, :name,
+ { selected: issuable.label_ids }, multiple: true, class: 'select2', data: { placeholder: "Select labels" }
+ - else
+ %span.light No labels yet.
+ - if can? current_user, :admin_label, issuable.project
+ %div
+ = link_to 'Create new label', new_namespace_project_label_path(issuable.project.namespace, issuable.project), target: :blank, class: "prepend-top-5 inline"
+ - if has_due_date
+ .col-lg-6
+ .form-group
+ = f.label :due_date, "Due date", class: "control-label"
+ = f.hidden_field :due_date, id: "issuable-due-date"
+ .col-sm-10
+ .datepicker
- if issuable.can_move?(current_user)
%hr
@@ -114,6 +122,13 @@
- if @merge_request.new_record?
&nbsp;
= link_to 'Change branches', mr_change_branches_path(@merge_request)
+ - if @merge_request.can_remove_source_branch?(current_user)
+ .form-group
+ .col-sm-10.col-sm-offset-2
+ .checkbox
+ = label_tag 'merge_request[force_remove_source_branch]' do
+ = check_box_tag 'merge_request[force_remove_source_branch]', '1', @merge_request.force_remove_source_branch?
+ Remove source branch when merge request is accepted.
- is_footer = !(issuable.is_a?(MergeRequest) && issuable.new_record?)
.row-content-block{class: (is_footer ? "footer-block" : "middle-block")}