Commit c9fe9f59 authored by Alfredo Sumaran's avatar Alfredo Sumaran

Merge remote-tracking branch 'origin/master' into issue_3799

parents 80ae3790 c22c4048
...@@ -115,6 +115,11 @@ bundler:audit: ...@@ -115,6 +115,11 @@ bundler:audit:
script: script:
- "bundle exec bundle-audit check --update --ignore OSVDB-115941" - "bundle exec bundle-audit check --update --ignore OSVDB-115941"
db-migrate-reset:
stage: test
script:
- RAILS_ENV=test bundle exec rake db:migrate:reset
# Ruby 2.2 jobs # Ruby 2.2 jobs
spec:feature:ruby22: spec:feature:ruby22:
......
...@@ -728,7 +728,7 @@ Metrics/ParameterLists: ...@@ -728,7 +728,7 @@ Metrics/ParameterLists:
# A complexity metric geared towards measuring complexity for a human reader. # A complexity metric geared towards measuring complexity for a human reader.
Metrics/PerceivedComplexity: Metrics/PerceivedComplexity:
Enabled: true Enabled: true
Max: 17 Max: 18
#################### Lint ################################ #################### Lint ################################
......
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
v 8.8.0 (unreleased)
v 8.7.0 (unreleased) v 8.7.0 (unreleased)
- Gitlab::GitAccess and Gitlab::GitAccessWiki are now instrumented
- The number of InfluxDB points stored per UDP packet can now be configured
- Fix error when cross-project label reference used with non-existent project
- Transactions for /internal/allowed now have an "action" tag set - Transactions for /internal/allowed now have an "action" tag set
- Method instrumentation now uses Module#prepend instead of aliasing methods - Method instrumentation now uses Module#prepend instead of aliasing methods
- Repository.clean_old_archives is now instrumented - Repository.clean_old_archives is now instrumented
...@@ -11,22 +16,26 @@ v 8.7.0 (unreleased) ...@@ -11,22 +16,26 @@ v 8.7.0 (unreleased)
- Developers can now add custom tags to transactions - Developers can now add custom tags to transactions
- Loading of an issue's referenced merge requests and related branches is now done asynchronously - Loading of an issue's referenced merge requests and related branches is now done asynchronously
- Enable gzip for assets, makes the page size significantly smaller. !3544 / !3632 (Connor Shea) - Enable gzip for assets, makes the page size significantly smaller. !3544 / !3632 (Connor Shea)
- Add support to cherry-pick any commit into any branch in the web interface (Minqi Pan)
- Project switcher uses new dropdown styling - Project switcher uses new dropdown styling
- Load award emoji images separately unless opening the full picker. Saves several hundred KBs of data for most pages. (Connor Shea) - Load award emoji images separately unless opening the full picker. Saves several hundred KBs of data for most pages. (Connor Shea)
- Do not include award_emojis in issue and merge_request comment_count !3610 (Lucas Charles) - Do not include award_emojis in issue and merge_request comment_count !3610 (Lucas Charles)
- Restrict user profiles when public visibility level is restricted. - Restrict user profiles when public visibility level is restricted.
- Add ability set due date to issues, sort and filter issues by due date (Mehmet Beydogan)
- All images in discussions and wikis now link to their source files !3464 (Connor Shea). - All images in discussions and wikis now link to their source files !3464 (Connor Shea).
- Return status code 303 after a branch DELETE operation to avoid project deletion (Stan Hu) - Return status code 303 after a branch DELETE operation to avoid project deletion (Stan Hu)
- Add setting for customizing the list of trusted proxies !3524 - Add setting for customizing the list of trusted proxies !3524
- Allow projects to be transfered to a lower visibility level group - Allow projects to be transfered to a lower visibility level group
- Fix `signed_in_ip` being set to 127.0.0.1 when using a reverse proxy !3524 - Fix `signed_in_ip` being set to 127.0.0.1 when using a reverse proxy !3524
- Improved Markdown rendering performance !3389 - Improved Markdown rendering performance !3389
- Make shared runners text in box configurable
- Don't attempt to look up an avatar in repo if repo directory does not exist (Stan Hu) - Don't attempt to look up an avatar in repo if repo directory does not exist (Stan Hu)
- API: Ability to subscribe and unsubscribe from issues and merge requests (Robert Schilling) - API: Ability to subscribe and unsubscribe from issues and merge requests (Robert Schilling)
- Expose project badges in project settings - Expose project badges in project settings
- Make /profile/keys/new redirect to /profile/keys for back-compat. !3717 - Make /profile/keys/new redirect to /profile/keys for back-compat. !3717
- Preserve time notes/comments have been updated at when moving issue - Preserve time notes/comments have been updated at when moving issue
- Make HTTP(s) label consistent on clone bar (Stan Hu) - Make HTTP(s) label consistent on clone bar (Stan Hu)
- Add support for `after_script`, requires Runner 1.2 (Kamil Trzciński)
- Expose label description in API (Mariusz Jachimowicz) - Expose label description in API (Mariusz Jachimowicz)
- API: Ability to update a group (Robert Schilling) - API: Ability to update a group (Robert Schilling)
- API: Ability to move issues (Robert Schilling) - API: Ability to move issues (Robert Schilling)
...@@ -34,6 +43,8 @@ v 8.7.0 (unreleased) ...@@ -34,6 +43,8 @@ v 8.7.0 (unreleased)
- Fix a bug whith trailing slash in teamcity_url (Charles May) - Fix a bug whith trailing slash in teamcity_url (Charles May)
- Allow back dating on issues when created or updated through the API - Allow back dating on issues when created or updated through the API
- Allow back dating on issue notes when created through the API - Allow back dating on issue notes when created through the API
- Propose license template when creating a new LICENSE file
- API: Expose /licenses and /licenses/:key
- Fix avatar stretching by providing a cropping feature - Fix avatar stretching by providing a cropping feature
- API: Expose `subscribed` for issues and merge requests (Robert Schilling) - API: Expose `subscribed` for issues and merge requests (Robert Schilling)
- Allow SAML to handle external users based on user's information !3530 - Allow SAML to handle external users based on user's information !3530
...@@ -41,6 +52,7 @@ v 8.7.0 (unreleased) ...@@ -41,6 +52,7 @@ v 8.7.0 (unreleased)
- Add endpoints to archive or unarchive a project !3372 - Add endpoints to archive or unarchive a project !3372
- Fix a bug whith trailing slash in bamboo_url - Fix a bug whith trailing slash in bamboo_url
- Add links to CI setup documentation from project settings and builds pages - Add links to CI setup documentation from project settings and builds pages
- Display project members page to all members
- Handle nil descriptions in Slack issue messages (Stan Hu) - Handle nil descriptions in Slack issue messages (Stan Hu)
- Add automated repository integrity checks - Add automated repository integrity checks
- API: Expose open_issues_count, closed_issues_count, open_merge_requests_count for labels (Robert Schilling) - API: Expose open_issues_count, closed_issues_count, open_merge_requests_count for labels (Robert Schilling)
...@@ -51,6 +63,7 @@ v 8.7.0 (unreleased) ...@@ -51,6 +63,7 @@ v 8.7.0 (unreleased)
- Use rugged to change HEAD in Project#change_head (P.S.V.R) - Use rugged to change HEAD in Project#change_head (P.S.V.R)
- API: Ability to filter milestones by state `active` and `closed` (Robert Schilling) - API: Ability to filter milestones by state `active` and `closed` (Robert Schilling)
- API: Fix milestone filtering by `iid` (Robert Schilling) - API: Fix milestone filtering by `iid` (Robert Schilling)
- Make before_script and after_script overridable on per-job (Kamil Trzciński)
- API: Delete notes of issues, snippets, and merge requests (Robert Schilling) - API: Delete notes of issues, snippets, and merge requests (Robert Schilling)
- Implement 'Groups View' as an option for dashboard preferences !3379 (Elias W.) - Implement 'Groups View' as an option for dashboard preferences !3379 (Elias W.)
- Better errors handling when creating milestones inside groups - Better errors handling when creating milestones inside groups
...@@ -58,6 +71,7 @@ v 8.7.0 (unreleased) ...@@ -58,6 +71,7 @@ v 8.7.0 (unreleased)
- Hide `Create a group` help block when creating a new project in a group - Hide `Create a group` help block when creating a new project in a group
- Implement 'TODOs View' as an option for dashboard preferences !3379 (Elias W.) - Implement 'TODOs View' as an option for dashboard preferences !3379 (Elias W.)
- Allow issues and merge requests to be assigned to the author !2765 - Allow issues and merge requests to be assigned to the author !2765
- Make Ci::Commit to group only similar builds and make it stateful (ref, tag)
- Gracefully handle notes on deleted commits in merge requests (Stan Hu) - Gracefully handle notes on deleted commits in merge requests (Stan Hu)
- Decouple membership and notifications - Decouple membership and notifications
- Fix creation of merge requests for orphaned branches (Stan Hu) - Fix creation of merge requests for orphaned branches (Stan Hu)
...@@ -71,29 +85,48 @@ v 8.7.0 (unreleased) ...@@ -71,29 +85,48 @@ v 8.7.0 (unreleased)
- API: Do not leak group existence via return code (Robert Schilling) - API: Do not leak group existence via return code (Robert Schilling)
- ClosingIssueExtractor regex now also works with colons. e.g. "Fixes: #1234" !3591 - ClosingIssueExtractor regex now also works with colons. e.g. "Fixes: #1234" !3591
- Update number of Todos in the sidebar when it's marked as "Done". !3600 - Update number of Todos in the sidebar when it's marked as "Done". !3600
- Sanitize branch names created for confidential issues
- API: Expose 'updated_at' for issue, snippet, and merge request notes (Robert Schilling) - API: Expose 'updated_at' for issue, snippet, and merge request notes (Robert Schilling)
- API: User can leave a project through the API when not master or owner. !3613 - API: User can leave a project through the API when not master or owner. !3613
- Fix repository cache invalidation issue when project is recreated with an empty repo (Stan Hu) - Fix repository cache invalidation issue when project is recreated with an empty repo (Stan Hu)
- Fix: Allow empty recipients list for builds emails service when pushed is added (Frank Groeneveld) - Fix: Allow empty recipients list for builds emails service when pushed is added (Frank Groeneveld)
- Improved markdown forms - Improved markdown forms
- Diff design updates (colors, button styles, etc)
- Copying and pasting a diff no longer pastes the line numbers or +/-
- Add null check to formData when updating profile content to fix Firefox bug
- Disable spellcheck and autocorrect for username field in admin page
- Delete tags using Rugged for performance reasons (Robert Schilling) - Delete tags using Rugged for performance reasons (Robert Schilling)
- Add Slack notifications when Wiki is edited (Sebastian Klier)
- Diffs load at the correct point when linking from from number - Diffs load at the correct point when linking from from number
- Selected diff rows highlight - Selected diff rows highlight
- Fix emoji categories in the emoji picker - Fix emoji categories in the emoji picker
- API: Properly display annotated tags for GET /projects/:id/repository/tags (Robert Schilling)
- Add encrypted credentials for imported projects and migrate old ones - Add encrypted credentials for imported projects and migrate old ones
- Properly format all merge request references with ! rather than # !3740 (Ben Bodenmiller)
- Author and participants are displayed first on users autocompletion - Author and participants are displayed first on users autocompletion
- Show number sign on external issue reference text (Florent Baldino) - Show number sign on external issue reference text (Florent Baldino)
- Updated print style for issues - Updated print style for issues
- Use GitHub Issue/PR number as iid to keep references - Use GitHub Issue/PR number as iid to keep references
- Import GitHub labels - Import GitHub labels
- Add option to filter by "Owned projects" on dashboard page - Add option to filter by "Owned projects" on dashboard page
- Import GitHub milestones
- Fix emoji catgories in the emoji picker
- Execute system web hooks on push to the project
- Allow enable/disable push events for system hooks
- Fix GitHub project's link in the import page when provider has a custom URL
- Add RAW build trace output and button on build page
- Add incremental build trace update into CI API
v 8.6.7
- Fix persistent XSS vulnerability in `commit_person_link` helper
- Fix persistent XSS vulnerability in Label and Milestone dropdowns
- Fix vulnerability that made it possible to enumerate private projects belonging to group
v 8.6.6 v 8.6.6
- Expire the exists cache before deletion to ensure project dir actually exists (Stan Hu). !3413 - Expire the exists cache before deletion to ensure project dir actually exists (Stan Hu). !3413
- Fix error on language detection when repository has no HEAD (e.g., master branch) (Jeroen Bobbeldijk). !3654 - Fix error on language detection when repository has no HEAD (e.g., master branch) (Jeroen Bobbeldijk). !3654
- Fix revoking of authorized OAuth applications (Connor Shea). !3690 - Fix revoking of authorized OAuth applications (Connor Shea). !3690
- Fix error on language detection when repository has no HEAD (e.g., master branch). !3654 (Jeroen Bobbeldijk) - Fix error on language detection when repository has no HEAD (e.g., master branch). !3654 (Jeroen Bobbeldijk)
- Project switcher uses new dropdown styling
- Issuable header is consistent between issues and merge requests - Issuable header is consistent between issues and merge requests
- Improved spacing in issuable header on mobile - Improved spacing in issuable header on mobile
...@@ -225,6 +258,9 @@ v 8.6.0 ...@@ -225,6 +258,9 @@ v 8.6.0
- Trigger a todo for mentions on commits page - Trigger a todo for mentions on commits page
- Let project owners and admins soft delete issues and merge requests - Let project owners and admins soft delete issues and merge requests
v 8.5.11
- Fix persistent XSS vulnerability in `commit_person_link` helper
v 8.5.10 v 8.5.10
- Fix a 2FA authentication spoofing vulnerability. - Fix a 2FA authentication spoofing vulnerability.
...@@ -372,6 +408,9 @@ v 8.5.0 ...@@ -372,6 +408,9 @@ v 8.5.0
- Show label row when filtering issues or merge requests by label (Nuttanart Pornprasitsakul) - Show label row when filtering issues or merge requests by label (Nuttanart Pornprasitsakul)
- Add Todos - Add Todos
v 8.4.9
- Fix persistent XSS vulnerability in `commit_person_link` helper
v 8.4.8 v 8.4.8
- Fix a 2FA authentication spoofing vulnerability. - Fix a 2FA authentication spoofing vulnerability.
...@@ -494,6 +533,9 @@ v 8.4.0 ...@@ -494,6 +533,9 @@ v 8.4.0
- Add IP check against DNSBLs at account sign-up - Add IP check against DNSBLs at account sign-up
- Added cache:key to .gitlab-ci.yml allowing to fine tune the caching - Added cache:key to .gitlab-ci.yml allowing to fine tune the caching
v 8.3.8
- Fix persistent XSS vulnerability in `commit_person_link` helper
v 8.3.7 v 8.3.7
- Fix a 2FA authentication spoofing vulnerability. - Fix a 2FA authentication spoofing vulnerability.
......
...@@ -323,6 +323,7 @@ request is as follows: ...@@ -323,6 +323,7 @@ request is as follows:
[shell command guidelines](doc/development/shell_commands.md) [shell command guidelines](doc/development/shell_commands.md)
1. If your code creates new files on disk please read the 1. If your code creates new files on disk please read the
[shared files guidelines](doc/development/shared_files.md). [shared files guidelines](doc/development/shared_files.md).
1. When writing commit messages please follow [these](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) [guidelines](http://chris.beams.io/posts/git-commit/).
The **official merge window** is in the beginning of the month from the 1st to The **official merge window** is in the beginning of the month from the 1st to
the 7th day of the month. This is the best time to submit an MR and get the 7th day of the month. This is the best time to submit an MR and get
......
...@@ -190,6 +190,9 @@ gem 'babosa', '~> 1.0.2' ...@@ -190,6 +190,9 @@ gem 'babosa', '~> 1.0.2'
# Sanitizes SVG input # Sanitizes SVG input
gem "loofah", "~> 2.0.3" gem "loofah", "~> 2.0.3"
# Working with license
gem 'licensee', '~> 8.0.0'
# Protect against bruteforcing # Protect against bruteforcing
gem "rack-attack", '~> 4.3.1' gem "rack-attack", '~> 4.3.1'
......
...@@ -452,6 +452,8 @@ GEM ...@@ -452,6 +452,8 @@ GEM
addressable (~> 2.3) addressable (~> 2.3)
letter_opener (1.1.2) letter_opener (1.1.2)
launchy (~> 2.2) launchy (~> 2.2)
licensee (8.0.0)
rugged (>= 0.24b)
listen (3.0.5) listen (3.0.5)
rb-fsevent (>= 0.9.3) rb-fsevent (>= 0.9.3)
rb-inotify (>= 0.9) rb-inotify (>= 0.9)
...@@ -957,6 +959,7 @@ DEPENDENCIES ...@@ -957,6 +959,7 @@ DEPENDENCIES
jquery-ui-rails (~> 5.0.0) jquery-ui-rails (~> 5.0.0)
kaminari (~> 0.16.3) kaminari (~> 0.16.3)
letter_opener (~> 1.1.2) letter_opener (~> 1.1.2)
licensee (~> 8.0.0)
loofah (~> 2.0.3) loofah (~> 2.0.3)
mail_room (~> 0.6.1) mail_room (~> 0.6.1)
method_source (~> 0.8) method_source (~> 0.8)
......
...@@ -105,6 +105,25 @@ sensitive as to how you word things. Use Emoji to express your feelings (heart, ...@@ -105,6 +105,25 @@ sensitive as to how you word things. Use Emoji to express your feelings (heart,
star, smile, etc.). Some good tips about giving feedback to merge requests is in star, smile, etc.). Some good tips about giving feedback to merge requests is in
the [Thoughtbot code review guide]. the [Thoughtbot code review guide].
## Feature Freeze
5 working days before the 22nd the stable branches for the upcoming release will
be frozen for major changes. Merge requests may still be merged into master
during this period. By freezing the stable branches prior to a release there's
no need to worry about last minute merge requests potentially breaking a lot of
things.
What is considered to be a major change is determined on a case by case basis as
this definition depends very much on the context of changes. For example, a 5
line change might have a big impact on the entire application. Ultimately the
decision will be made by those reviewing a merge request and the release
manager.
During the feature freeze all merge requests that are meant to go into the next
release should have the correct milestone assigned _and_ have the label
~"Pick into Stable" set. Merge requests without a milestone and this label will
not be merged into any stable branches.
## Copy & paste responses ## Copy & paste responses
### Improperly formatted issue ### Improperly formatted issue
......
8.7.0-pre 8.8.0-pre
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
group_projects_path: "/api/:version/groups/:id/projects.json" group_projects_path: "/api/:version/groups/:id/projects.json"
projects_path: "/api/:version/projects.json" projects_path: "/api/:version/projects.json"
labels_path: "/api/:version/projects/:id/labels" labels_path: "/api/:version/projects/:id/labels"
license_path: "/api/:version/licenses/:key"
group: (group_id, callback) -> group: (group_id, callback) ->
url = Api.buildUrl(Api.group_path) url = Api.buildUrl(Api.group_path)
...@@ -92,6 +93,16 @@ ...@@ -92,6 +93,16 @@
).done (projects) -> ).done (projects) ->
callback(projects) callback(projects)
# Return text for a specific license
licenseText: (key, data, callback) ->
url = Api.buildUrl(Api.license_path).replace(':key', key)
$.ajax(
url: url
data: data
).done (license) ->
callback(license)
buildUrl: (url) -> buildUrl: (url) ->
url = gon.relative_url_root + url if gon.relative_url_root? url = gon.relative_url_root + url if gon.relative_url_root?
return url.replace(':version', gon.api_version) return url.replace(':version', gon.api_version)
class @BlobLicenseSelector
licenseRegex: /^(.+\/)?(licen[sc]e|copying)($|\.)/i
constructor: (editor) ->
@$licenseSelector = $('.js-license-selector')
$fileNameInput = $('#file_name')
initialFileNameValue = if $fileNameInput.length
$fileNameInput.val()
else if $('.editor-file-name').length
$('.editor-file-name').text().trim()
@toggleLicenseSelector(initialFileNameValue)
if $fileNameInput
$fileNameInput.on 'keyup blur', (e) =>
@toggleLicenseSelector($(e.target).val())
$('select.license-select').on 'change', (e) ->
data =
project: $(this).data('project')
fullname: $(this).data('fullname')
Api.licenseText $(this).val(), data, (license) ->
editor.setValue(license.content, -1)
toggleLicenseSelector: (fileName) =>
if @licenseRegex.test(fileName)
@$licenseSelector.show()
else
@$licenseSelector.hide()
class @EditBlob class @EditBlob
constructor: (assets_path, mode)-> constructor: (assets_path, ace_mode = null) ->
ace.config.set "modePath", assets_path + '/ace' ace.config.set "modePath", "#{assets_path}/ace"
ace.config.loadModule "ace/ext/searchbox" ace.config.loadModule "ace/ext/searchbox"
if mode @editor = ace.edit("editor")
ace_mode = mode @editor.focus()
editor = ace.edit("editor") @editor.getSession().setMode "ace/mode/#{ace_mode}" if ace_mode
editor.focus()
@editor = editor
if ace_mode
editor.getSession().setMode "ace/mode/" + ace_mode
# Before a form submission, move the content from the Ace editor into the # Before a form submission, move the content from the Ace editor into the
# submitted textarea # submitted textarea
$('form').submit -> $('form').submit =>
$("#file-content").val(editor.getValue()) $("#file-content").val(@editor.getValue())
@initModePanesAndLinks()
new BlobLicenseSelector(@editor)
editModePanes = $(".js-edit-mode-pane") initModePanesAndLinks: ->
editModeLinks = $(".js-edit-mode a") @$editModePanes = $(".js-edit-mode-pane")
editModeLinks.click (event) -> @$editModeLinks = $(".js-edit-mode a")
event.preventDefault() @$editModeLinks.click @editModeLinkClickHandler
currentLink = $(this)
paneId = currentLink.attr("href")
currentPane = editModePanes.filter(paneId)
editModeLinks.parent().removeClass "active hover"
currentLink.parent().addClass "active hover"
editModePanes.hide()
if paneId is "#preview"
currentPane.fadeIn 200
$.post currentLink.data("preview-url"),
content: editor.getValue()
, (response) ->
currentPane.empty().append response
currentPane.syntaxHighlight()
return
else editModeLinkClickHandler: (event) =>
currentPane.fadeIn 200 event.preventDefault()
editor.focus() currentLink = $(event.target)
return paneId = currentLink.attr("href")
currentPane = @$editModePanes.filter(paneId)
@$editModeLinks.parent().removeClass "active hover"
currentLink.parent().addClass "active hover"
@$editModePanes.hide()
currentPane.fadeIn 200
if paneId is "#preview"
$.post currentLink.data("preview-url"),
content: @editor.getValue()
, (response) ->
currentPane.empty().append response
currentPane.syntaxHighlight()
editor: -> else
return @editor @editor.focus()
class @NewBlob
constructor: (assets_path, mode)->
ace.config.set "modePath", assets_path + '/ace'
ace.config.loadModule "ace/ext/searchbox"
if mode
ace_mode = mode
editor = ace.edit("editor")
editor.focus()
@editor = editor
if ace_mode
editor.getSession().setMode "ace/mode/" + ace_mode
# Before a form submission, move the content from the Ace editor into the
# submitted textarea
$('form').submit ->
$("#file-content").val(editor.getValue())
editor: ->
return @editor
class @CommitsList class @CommitsList
@timer = null @timer = null
@init: (ref, limit) -> @init: (limit) ->
$("body").on "click", ".day-commits-table li.commit", (event) -> $("body").on "click", ".day-commits-table li.commit", (event) ->
if event.target.nodeName != "A" if event.target.nodeName != "A"
location.href = $(this).attr("url") location.href = $(this).attr("url")
......
...@@ -17,6 +17,7 @@ class Dispatcher ...@@ -17,6 +17,7 @@ class Dispatcher
switch page switch page
when 'projects:issues:index' when 'projects:issues:index'
Issues.init() Issues.init()
Issuable.init()
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsNavigation()
when 'projects:issues:show' when 'projects:issues:show'
new Issue() new Issue()
...@@ -57,7 +58,7 @@ class Dispatcher ...@@ -57,7 +58,7 @@ class Dispatcher
new ZenMode() new ZenMode()
when 'projects:merge_requests:index' when 'projects:merge_requests:index'
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsNavigation()
MergeRequests.init() Issuable.init()
when 'dashboard:activity' when 'dashboard:activity'
new Activities() new Activities()
when 'dashboard:projects:starred' when 'dashboard:projects:starred'
......
class @DueDateSelect
constructor: ->
$loading = $('.js-issuable-update .due_date')
.find('.block-loading')
.hide()
$('.js-due-date-select').each (i, dropdown) ->
$dropdown = $(dropdown)
$dropdownParent = $dropdown.closest('.dropdown')
$datePicker = $dropdownParent.find('.js-due-date-calendar')
$block = $dropdown.closest('.block')
$selectbox = $dropdown.closest('.selectbox')
$value = $block.find('.value')
$sidebarValue = $('.js-due-date-sidebar-value', $block)
fieldName = $dropdown.data('field-name')
abilityName = $dropdown.data('ability-name')
issueUpdateURL = $dropdown.data('issue-update')
$dropdown.glDropdown(
hidden: ->
$selectbox.hide()
$value.removeAttr('style')
)
addDueDate = ->
# Create the post date
value = $("input[name='#{fieldName}']").val()
date = new Date value.replace(new RegExp('-', 'g'), ',')
mediumDate = $.datepicker.formatDate 'M d, yy', date
data = {}
data[abilityName] = {}
data[abilityName].due_date = value
$.ajax(
type: 'PUT'
url: issueUpdateURL
data: data
beforeSend: ->
$loading.fadeIn()
$dropdown.trigger('loading.gl.dropdown')
$selectbox.hide()
$value.removeAttr('style')
$value.html(mediumDate)
$sidebarValue.html(mediumDate)
).done (data) ->
$dropdown.trigger('loaded.gl.dropdown')
$dropdown.dropdown('toggle')
$loading.fadeOut()
$datePicker.datepicker(
dateFormat: 'yy-mm-dd',
defaultDate: $("input[name='#{fieldName}']").val()
altField: "input[name='#{fieldName}']"
onSelect: ->
addDueDate()
)
$(document)
.off 'click', '.ui-datepicker-header a'
.on 'click', '.ui-datepicker-header a', (e) ->
e.stopImmediatePropagation()
...@@ -154,6 +154,9 @@ class GitLabDropdown ...@@ -154,6 +154,9 @@ class GitLabDropdown
@fullData = data @fullData = data
@parseData @fullData @parseData @fullData
if @options.filterable
@filterInput.trigger 'keyup'
} }
# Init filterable # Init filterable
...@@ -218,6 +221,9 @@ class GitLabDropdown ...@@ -218,6 +221,9 @@ class GitLabDropdown
menu.toggleClass PAGE_TWO_CLASS menu.toggleClass PAGE_TWO_CLASS
# Focus first visible input on active page
@dropdown.find('[class^="dropdown-page-"]:visible :text:visible:first').focus()
parseData: (data) -> parseData: (data) ->
@renderedData = data @renderedData = data
...@@ -237,7 +243,8 @@ class GitLabDropdown ...@@ -237,7 +243,8 @@ class GitLabDropdown
shouldPropagate: (e) => shouldPropagate: (e) =>
if @options.multiSelect if @options.multiSelect
$target = $(e.target) $target = $(e.target)
if not $target.hasClass('dropdown-menu-close') and not $target.hasClass('dropdown-menu-close-icon')
if not $target.hasClass('dropdown-menu-close') and not $target.hasClass('dropdown-menu-close-icon') and not $target.data('is-link')
e.stopPropagation() e.stopPropagation()
return false return false
else else
...@@ -372,7 +379,6 @@ class GitLabDropdown ...@@ -372,7 +379,6 @@ class GitLabDropdown
selectedObject = @renderedData[selectedIndex] selectedObject = @renderedData[selectedIndex]
value = if @options.id then @options.id(selectedObject, el) else selectedObject.id value = if @options.id then @options.id(selectedObject, el) else selectedObject.id
field = @dropdown.parent().find("input[name='#{fieldName}'][value='#{value}']") field = @dropdown.parent().find("input[name='#{fieldName}'][value='#{value}']")
if el.hasClass(ACTIVE_CLASS) if el.hasClass(ACTIVE_CLASS)
el.removeClass(ACTIVE_CLASS) el.removeClass(ACTIVE_CLASS)
field.remove() field.remove()
...@@ -383,13 +389,13 @@ class GitLabDropdown ...@@ -383,13 +389,13 @@ class GitLabDropdown
else else
selectedObject selectedObject
else else
if !value? if not @options.multiSelect or el.hasClass('dropdown-clear-active')
field.remove()
if not @options.multiSelect
@dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS @dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS
@dropdown.parent().find("input[name='#{fieldName}']").remove() @dropdown.parent().find("input[name='#{fieldName}']").remove()
if !value?
field.remove()
# Toggle active class for the tick mark # Toggle active class for the tick mark
el.addClass ACTIVE_CLASS el.addClass ACTIVE_CLASS
......
@Issuable =
init: ->
Issuable.initTemplates()
Issuable.initSearch()
initTemplates: ->
Issuable.labelRow = _.template(
'<% _.each(labels, function(label){ %>
<span class="label-row">
<a href="#"><span class="label color-label has-tooltip" style="background-color: <%= label.color %>; color: <%= label.text_color %>" title="<%= _.escape(label.description) %>" data-container="body"><%= _.escape(label.title) %></span></a>
</span>
<% }); %>'
)
initSearch: ->
@timer = null
$('#issue_search')
.off 'keyup'
.on 'keyup', ->
clearTimeout(@timer)
@timer = setTimeout( ->
Issuable.filterResults $('#issue_search_form')
, 500)
toggleLabelFilters: ->
$filteredLabels = $('.filtered-labels')
if $filteredLabels.find('.label-row').length > 0
$filteredLabels.removeClass('hidden')
else
$filteredLabels.addClass('hidden')
filterResults: (form) =>
formData = form.serialize()
$('.issues-holder, .merge-requests-holder').css('opacity', '0.5')
formAction = form.attr('action')
issuesUrl = formAction
issuesUrl += ("#{if formAction.indexOf('?') < 0 then '?' else '&'}")
issuesUrl += formData
$.ajax
type: 'GET'
url: formAction
data: formData
complete: ->
$('.issues-holder, .merge-requests-holder').css('opacity', '1.0')
success: (data) ->
$('.issues-holder, .merge-requests-holder').html(data.html)
# Change url so if user reload a page - search results are saved
history.replaceState {page: issuesUrl}, document.title, issuesUrl
Issuable.reload()
Issuable.updateStateFilters()
$filteredLabels = $('.filtered-labels')
if typeof Issuable.labelRow is 'function'
$filteredLabels.html(Issuable.labelRow(data))
Issuable.toggleLabelFilters()
dataType: "json"
reload: ->
if Issues.created
Issues.initChecks()
$('#filter_issue_search').val($('#issue_search').val())
updateStateFilters: ->
stateFilters = $('.issues-state-filters')
newParams = {}
paramKeys = ['author_id', 'milestone_title', 'assignee_id', 'issue_search']
for paramKey in paramKeys
newParams[paramKey] = gl.utils.getParameterValues(paramKey)[0] or ''
if stateFilters.length
stateFilters.find('a').each ->
initialUrl = gl.utils.removeParamQueryString($(this).attr('href'), 'label_name[]')
labelNameValues = gl.utils.getParameterValues('label_name[]')
if labelNameValues
labelNameQueryString = ("label_name[]=#{value}" for value in labelNameValues).join('&')
newUrl = "#{gl.utils.mergeUrlParams(newParams, initialUrl)}&#{labelNameQueryString}"
else
newUrl = gl.utils.mergeUrlParams(newParams, initialUrl)
$(this).attr 'href', newUrl
...@@ -9,21 +9,29 @@ class @IssuableContext ...@@ -9,21 +9,29 @@ class @IssuableContext
$(".issuable-sidebar .inline-update").on "change", ".js-assignee", -> $(".issuable-sidebar .inline-update").on "change", ".js-assignee", ->
$(this).submit() $(this).submit()
$(document).off("click", ".edit-link").on "click",".edit-link", (e) -> $(document)
$block = $(@).parents('.block') .off 'click', '.dropdown-content a'
$selectbox = $block.find('.selectbox') .on 'click', '.dropdown-content a', (e) ->
if $selectbox.is(':visible') e.preventDefault()
$selectbox.hide()
$block.find('.value').show() $(document)
else .off 'click', '.edit-link'
$selectbox.show() .on 'click', '.edit-link', (e) ->
$block.find('.value').hide() e.preventDefault()
if $selectbox.is(':visible') $block = $(@).parents('.block')
setTimeout (-> $selectbox = $block.find('.selectbox')
$block.find('.dropdown-menu-toggle').trigger 'click' if $selectbox.is(':visible')
), 0 $selectbox.hide()
$block.find('.value').show()
else
$selectbox.show()
$block.find('.value').hide()
if $selectbox.is(':visible')
setTimeout ->
$block.find('.dropdown-menu-toggle').trigger 'click'
, 0
$(".right-sidebar").niceScroll() $(".right-sidebar").niceScroll()
......
@Issues = @Issues =
init: -> init: ->
Issues.initSearch() Issues.created = true
Issues.initChecks() Issues.initChecks()
$("body").on "ajax:success", ".close_issue, .reopen_issue", -> $("body").on "ajax:success", ".close_issue, .reopen_issue", ->
...@@ -15,10 +15,6 @@ ...@@ -15,10 +15,6 @@
else else
$(this).html totalIssues - 1 $(this).html totalIssues - 1
reload: ->
Issues.initChecks()
$('#filter_issue_search').val($('#issue_search').val())
initChecks: -> initChecks: ->
$(".check_all_issues").click -> $(".check_all_issues").click ->
$(".selected_issue").prop("checked", @checked) $(".selected_issue").prop("checked", @checked)
...@@ -26,51 +22,6 @@ ...@@ -26,51 +22,6 @@
$(".selected_issue").bind "change", Issues.checkChanged $(".selected_issue").bind "change", Issues.checkChanged
# Update state filters if present in page
updateStateFilters: ->
stateFilters = $('.issues-state-filters')
newParams = {}
paramKeys = ['author_id', 'label_name', 'milestone_title', 'assignee_id', 'issue_search']
for paramKey in paramKeys
newParams[paramKey] = gl.utils.getUrlParameter(paramKey) or ''
if stateFilters.length
stateFilters.find('a').each ->
initialUrl = $(this).attr 'href'
$(this).attr 'href', gl.utils.mergeUrlParams(newParams, initialUrl)
# Make sure we trigger ajax request only after user stop typing
initSearch: ->
@timer = null
$("#issue_search").keyup ->
clearTimeout(@timer)
@timer = setTimeout( ->
Issues.filterResults $("#issue_search_form")
, 500)
filterResults: (form) =>
$('.issues-holder, .merge-requests-holder').css("opacity", '0.5')
formAction = form.attr('action')
formData = form.serialize()
issuesUrl = formAction
issuesUrl += ("#{if formAction.indexOf("?") < 0 then '?' else '&'}")
issuesUrl += formData
$.ajax
type: "GET"
url: formAction
data: formData
complete: ->
$('.issues-holder, .merge-requests-holder').css("opacity", '1.0')
success: (data) ->
$('.issues-holder, .merge-requests-holder').html(data.html)
# Change url so if user reload a page - search results are saved
history.replaceState {page: issuesUrl}, document.title, issuesUrl
Issues.reload()
Issues.updateStateFilters()
dataType: "json"
checkChanged: -> checkChanged: ->
checked_issues = $(".selected_issue:checked") checked_issues = $(".selected_issue:checked")
if checked_issues.length > 0 if checked_issues.length > 0
......
...@@ -6,7 +6,7 @@ class @LabelsSelect ...@@ -6,7 +6,7 @@ class @LabelsSelect
labelUrl = $dropdown.data('labels') labelUrl = $dropdown.data('labels')
issueUpdateURL = $dropdown.data('issueUpdate') issueUpdateURL = $dropdown.data('issueUpdate')
selectedLabel = $dropdown.data('selected') selectedLabel = $dropdown.data('selected')
if selectedLabel? if selectedLabel? and not $dropdown.hasClass 'js-multiselect'
selectedLabel = selectedLabel.split(',') selectedLabel = selectedLabel.split(',')
newLabelField = $('#new_label_name') newLabelField = $('#new_label_name')
newColorField = $('#new_label_color') newColorField = $('#new_label_color')
...@@ -16,33 +16,32 @@ class @LabelsSelect ...@@ -16,33 +16,32 @@ class @LabelsSelect
abilityName = $dropdown.data('ability-name') abilityName = $dropdown.data('ability-name')
$selectbox = $dropdown.closest('.selectbox') $selectbox = $dropdown.closest('.selectbox')
$block = $selectbox.closest('.block') $block = $selectbox.closest('.block')
$form = $dropdown.closest('form')
$sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span') $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span')
$value = $block.find('.value') $value = $block.find('.value')
$loading = $block.find('.block-loading').fadeOut() $newLabelError = $('.js-label-error')
$colorPreview = $('.js-dropdown-label-color-preview')
if newLabelField.length $newLabelCreateButton = $('.js-new-label-btn')
$newLabelCreateButton = $('.js-new-label-btn')
$colorPreview = $('.js-dropdown-label-color-preview')
$newLabelError = $dropdown.parent().find('.js-label-error')
$newLabelError.hide()
# Suggested colors in the dropdown to chose from pre-chosen colors $newLabelError.hide()
$('.suggest-colors-dropdown a').on 'click', (e) -> $loading = $block.find('.block-loading').fadeOut()
issueURLSplit = issueUpdateURL.split('/') if issueUpdateURL? issueURLSplit = issueUpdateURL.split('/') if issueUpdateURL?
if issueUpdateURL if issueUpdateURL
labelHTMLTemplate = _.template( labelHTMLTemplate = _.template(
'<% _.each(labels, function(label){ %> '<% _.each(labels, function(label){ %>
<a href="<%= ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name=<%= label.title %>"> <a href="<%= ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name=<%= _.escape(label.title) %>">
<span class="label has-tooltip color-label" title="<%= label.description %>" style="background-color: <%= label.color %>;"> <span class="label has-tooltip color-label" title="<%= _.escape(label.description) %>" style="background-color: <%= label.color %>;">
<%= label.title %> <%= _.escape(label.title) %>
</span> </span>
</a> </a>
<% }); %>' <% }); %>'
); )
labelNoneHTMLTemplate = _.template('<div class="light">None</div>') labelNoneHTMLTemplate = _.template('<div class="light">None</div>')
if newLabelField.length and $dropdown.hasClass 'js-extra-options' if newLabelField.length
# Suggested colors in the dropdown to chose from pre-chosen colors
$('.suggest-colors-dropdown a').on "click", (e) -> $('.suggest-colors-dropdown a').on "click", (e) ->
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
...@@ -81,26 +80,25 @@ class @LabelsSelect ...@@ -81,26 +80,25 @@ class @LabelsSelect
enableLabelCreateButton = -> enableLabelCreateButton = ->
if newLabelField.val() isnt '' and newColorField.val() isnt '' if newLabelField.val() isnt '' and newColorField.val() isnt ''
$newLabelError.hide() $newLabelError.hide()
$('.js-new-label-btn').disable()
# Create new label with API
Api.newLabel projectId, {
name: newLabelField.val()
color: newColorField.val()
}, (label) ->
$('.js-new-label-btn').enable()
if label.message?
$newLabelError
.text label.message
.show()
else
$('.dropdown-menu-back', $dropdown.parent()).trigger 'click'
$newLabelCreateButton.enable() $newLabelCreateButton.enable()
else else
$newLabelCreateButton.disable() $newLabelCreateButton.disable()
saveLabel = ->
# Create new label with API
Api.newLabel projectId, {
name: newLabelField.val()
color: newColorField.val()
}, (label) ->
$newLabelCreateButton.enable()
if label.message?
$newLabelError
.text label.message
.show()
else
$('.dropdown-menu-back', $dropdown.parent()).trigger 'click'
newLabelField.on 'keyup change', enableLabelCreateButton newLabelField.on 'keyup change', enableLabelCreateButton
newColorField.on 'keyup change', enableLabelCreateButton newColorField.on 'keyup change', enableLabelCreateButton
...@@ -111,24 +109,7 @@ class @LabelsSelect ...@@ -111,24 +109,7 @@ class @LabelsSelect
.on 'click', (e) -> .on 'click', (e) ->
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
saveLabel()
if newLabelField.val() isnt '' and newColorField.val() isnt ''
$newLabelError.hide()
$('.js-new-label-btn').disable()
# Create new label with API
Api.newLabel projectId, {
name: newLabelField.val()
color: newColorField.val()
}, (label) ->
$('.js-new-label-btn').enable()
if label.message?
$newLabelError
.text label.message
.show()
else
$('.dropdown-menu-back', $dropdown.parent()).trigger 'click'
saveLabelData = -> saveLabelData = ->
selected = $dropdown selected = $dropdown
...@@ -171,7 +152,7 @@ class @LabelsSelect ...@@ -171,7 +152,7 @@ class @LabelsSelect
.find('a') .find('a')
.each((i) -> .each((i) ->
setTimeout(=> setTimeout(=>
glAnimate($(@), 'pulse') gl.animate.animate($(@), 'pulse')
,200 * i ,200 * i
) )
) )
...@@ -200,18 +181,23 @@ class @LabelsSelect ...@@ -200,18 +181,23 @@ class @LabelsSelect
callback data callback data
renderRow: (label) -> renderRow: (label) ->
selectedClass = '' removesAll = label.id is 0 or not label.id?
if $selectbox.find("input[type='hidden']\
[name='#{$dropdown.data('field-name')}']\ selectedClass = []
[value='#{label.id}']").length if $form.find("input[type='hidden']\
selectedClass = 'is-active' [name='#{$dropdown.data('fieldName')}']\
[value='#{this.id(label)}']").length
selectedClass.push 'is-active'
if $dropdown.hasClass('js-multiselect') and removesAll
selectedClass.push 'dropdown-clear-active'
color = if label.color? then "<span class='dropdown-label-box' style='background-color: #{label.color}'></span>" else "" color = if label.color? then "<span class='dropdown-label-box' style='background-color: #{label.color}'></span>" else ""
"<li> "<li>
<a href='#' class='#{selectedClass}'> <a href='#' class='#{selectedClass.join(' ')}'>
#{color} #{color}
#{label.title} #{_.escape(label.title)}
</a> </a>
</li>" </li>"
filterable: true filterable: true
...@@ -219,37 +205,56 @@ class @LabelsSelect ...@@ -219,37 +205,56 @@ class @LabelsSelect
fields: ['title'] fields: ['title']
selectable: true selectable: true
toggleLabel: (selected) -> toggleLabel: (selected, el) ->
selected_labels = $('.js-label-select').siblings('.dropdown-menu-labels').find('.is-active')
if selected and selected.title? if selected and selected.title?
selected.title if selected_labels.length > 1
"#{selected.title} +#{selected_labels.length - 1} more"
else
selected.title
else if not selected and selected_labels.length isnt 0
if selected_labels.length > 1
"#{$(selected_labels[0]).text()} +#{selected_labels.length - 1} more"
else if selected_labels.length is 1
$(selected_labels).text()
else else
defaultLabel defaultLabel
fieldName: $dropdown.data('field-name') fieldName: $dropdown.data('field-name')
id: (label) -> id: (label) ->
if label.isAny? if $dropdown.hasClass("js-filter-submit") and not label.isAny?
''
else if $dropdown.hasClass "js-filter-submit"
label.title label.title
else else
label.id label.id
hidden: -> hidden: ->
page = $('body').data 'page'
isIssueIndex = page is 'projects:issues:index'
isMRIndex = page is 'projects:merge_requests:index'
$selectbox.hide() $selectbox.hide()
# display:block overrides the hide-collapse rule # display:block overrides the hide-collapse rule
$value.removeAttr('style') $value.removeAttr('style')
if $dropdown.hasClass 'js-multiselect' if $dropdown.hasClass 'js-multiselect'
saveLabelData() if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex)
selectedLabels = $dropdown
.closest('form')
.find("input:hidden[name='#{$dropdown.data('fieldName')}']")
Issuable.filterResults $dropdown.closest('form')
else if $dropdown.hasClass('js-filter-submit')
$dropdown.closest('form').submit()
else
saveLabelData()
multiSelect: $dropdown.hasClass 'js-multiselect' multiSelect: $dropdown.hasClass 'js-multiselect'
clicked: (label) -> clicked: (label) ->
page = $('body').data 'page' page = $('body').data 'page'
isIssueIndex = page is 'projects:issues:index' isIssueIndex = page is 'projects:issues:index'
isMRIndex = page is page is 'projects:merge_requests:index' isMRIndex = page is 'projects:merge_requests:index'
if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex) if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex)
selectedLabel = label.title if not $dropdown.hasClass 'js-multiselect'
selectedLabel = label.title
Issues.filterResults $dropdown.closest('form') Issuable.filterResults $dropdown.closest('form')
else if $dropdown.hasClass 'js-filter-submit' else if $dropdown.hasClass 'js-filter-submit'
$dropdown.closest('form').submit() $dropdown.closest('form').submit()
else else
......
((w) -> ((w) ->
if not w.gl? then w.gl = {}
if not gl.animate? then gl.animate = {}
w.glAnimate = ($el, animation, done) -> gl.animate.animate = ($el, animation, options, done) ->
if options?.cssStart?
$el.css(options.cssStart)
$el $el
.removeClass() .removeClass(animation + ' animated')
.addClass(animation + ' animated') .addClass(animation + ' animated')
.one 'webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', -> .one 'webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', ->
$(this).removeClass() $(this).removeClass(animation + ' animated')
if done?
done()
if options?.cssEnd?
$el.css(options.cssEnd)
return return
return return
return
gl.animate.animateEach = ($els, animation, time, options, done) ->
dfd = $.Deferred()
if not $els.length
dfd.resolve()
$els.each((i) ->
setTimeout(=>
$this = $(@)
gl.animate.animate($this, animation, options, =>
if i is $els.length - 1
dfd.resolve()
if done?
done()
)
,time * i
)
return
)
return dfd.promise()
return
) window ) window
\ No newline at end of file
...@@ -3,16 +3,20 @@ ...@@ -3,16 +3,20 @@
w.gl ?= {} w.gl ?= {}
w.gl.utils ?= {} w.gl.utils ?= {}
w.gl.utils.getUrlParameter = (sParam) -> # Returns an array containing the value(s) of the
# of the key passed as an argument
w.gl.utils.getParameterValues = (sParam) ->
sPageURL = decodeURIComponent(window.location.search.substring(1)) sPageURL = decodeURIComponent(window.location.search.substring(1))
sURLVariables = sPageURL.split('&') sURLVariables = sPageURL.split('&')
sParameterName = undefined sParameterName = undefined
values = []
i = 0 i = 0
while i < sURLVariables.length while i < sURLVariables.length
sParameterName = sURLVariables[i].split('=') sParameterName = sURLVariables[i].split('=')
if sParameterName[0] is sParam if sParameterName[0] is sParam
return if sParameterName[1] is undefined then true else sParameterName[1] values.push(sParameterName[1])
i++ i++
values
# # # #
# @param {Object} params - url keys and value to merge # @param {Object} params - url keys and value to merge
...@@ -28,4 +32,12 @@ ...@@ -28,4 +32,12 @@
newUrl = "#{newUrl}#{(if newUrl.indexOf('?') > 0 then '&' else '?')}#{paramName}=#{paramValue}" newUrl = "#{newUrl}#{(if newUrl.indexOf('?') > 0 then '&' else '?')}#{paramName}=#{paramValue}"
newUrl newUrl
# removes parameter query string from url. returns the modified url
w.gl.utils.removeParamQueryString = (url, param) ->
url = decodeURIComponent(url)
urlVariables = url.split('&')
(
variables for variables in urlVariables when variables.indexOf(param) is -1
).join('&')
) window ) window
...@@ -87,8 +87,8 @@ class @MergeRequestTabs ...@@ -87,8 +87,8 @@ class @MergeRequestTabs
if window.location.hash if window.location.hash
navBarHeight = $('.navbar-gitlab').outerHeight() navBarHeight = $('.navbar-gitlab').outerHeight()
$el = $("#{container} #{window.location.hash}") $el = $("#{container} #{window.location.hash}:not(.match)")
$.scrollTo("#{container} #{window.location.hash}", offset: -navBarHeight) if $el.length $.scrollTo("#{container} #{window.location.hash}:not(.match)", offset: -navBarHeight) if $el.length
# Activate a tab based on the current action # Activate a tab based on the current action
activateTab: (action) -> activateTab: (action) ->
...@@ -176,12 +176,12 @@ class @MergeRequestTabs ...@@ -176,12 +176,12 @@ class @MergeRequestTabs
if locationHash isnt '' if locationHash isnt ''
hashClassString = ".#{locationHash.replace('#', '')}" hashClassString = ".#{locationHash.replace('#', '')}"
$diffLine = $(locationHash) $diffLine = $("#{locationHash}:not(.match)", $('#diffs'))
if $diffLine.is ':not(tr)' if not $diffLine.is 'tr'
$diffLine = $("td#{locationHash}, td#{hashClassString}") $diffLine = $('#diffs').find("td#{locationHash}, td#{hashClassString}")
else else
$diffLine = $('td', $diffLine) $diffLine = $diffLine.find('td')
if $diffLine.length if $diffLine.length
$diffLine.addClass 'hll' $diffLine.addClass 'hll'
......
#
# * Filter merge requests
#
@MergeRequests =
init: ->
MergeRequests.initSearch()
# Make sure we trigger ajax request only after user stop typing
initSearch: ->
@timer = null
$("#issue_search").keyup ->
clearTimeout(@timer)
@timer = setTimeout(MergeRequests.filterResults, 500)
filterResults: =>
form = $("#issue_search_form")
search = $("#issue_search").val()
$('.merge-requests-holder').css("opacity", '0.5')
issues_url = form.attr('action') + '?' + form.serialize()
$.ajax
type: "GET"
url: form.attr('action')
data: form.serialize()
complete: ->
$('.merge-requests-holder').css("opacity", '1.0')
success: (data) ->
$('.merge-requests-holder').html(data.html)
# Change url so if user reload a page - search results are saved
history.replaceState {page: issues_url}, document.title, issues_url
MergeRequests.reload()
dataType: "json"
reload: ->
$('#filter_issue_search').val($('#issue_search').val())
...@@ -24,7 +24,7 @@ class @MilestoneSelect ...@@ -24,7 +24,7 @@ class @MilestoneSelect
if issueUpdateURL if issueUpdateURL
milestoneLinkTemplate = _.template( milestoneLinkTemplate = _.template(
'<a href="/<%= namespace %>/<%= path %>/milestones/<%= iid %>"><%= title %></a>' '<a href="/<%= namespace %>/<%= path %>/milestones/<%= iid %>"><%= _.escape(title) %></a>'
) )
milestoneLinkNoneTemplate = '<div class="light">None</div>' milestoneLinkNoneTemplate = '<div class="light">None</div>'
...@@ -71,7 +71,7 @@ class @MilestoneSelect ...@@ -71,7 +71,7 @@ class @MilestoneSelect
defaultLabel defaultLabel
fieldName: $dropdown.data('field-name') fieldName: $dropdown.data('field-name')
text: (milestone) -> text: (milestone) ->
milestone.title _.escape(milestone.title)
id: (milestone) -> id: (milestone) ->
if !useId if !useId
milestone.name milestone.name
...@@ -97,7 +97,7 @@ class @MilestoneSelect ...@@ -97,7 +97,7 @@ class @MilestoneSelect
selectedMilestone = selected.name selectedMilestone = selected.name
else else
selectedMilestone = '' selectedMilestone = ''
Issues.filterResults $dropdown.closest('form') Issuable.filterResults $dropdown.closest('form')
else if $dropdown.hasClass('js-filter-submit') else if $dropdown.hasClass('js-filter-submit')
$dropdown.closest('form').submit() $dropdown.closest('form').submit()
else else
......
class @Todos class @Todos
constructor: (@name) -> constructor: (opts = {}) ->
{
@el = $('.js-todos-options')
} = opts
@perPage = @el.data('perPage')
@clearListeners() @clearListeners()
@initBtnListeners() @initBtnListeners()
...@@ -26,6 +32,7 @@ class @Todos ...@@ -26,6 +32,7 @@ class @Todos
dataType: 'json' dataType: 'json'
data: '_method': 'delete' data: '_method': 'delete'
success: (data) => success: (data) =>
@redirectIfNeeded data.count
@clearDone $this.closest('li') @clearDone $this.closest('li')
@updateBadges data @updateBadges data
...@@ -57,6 +64,40 @@ class @Todos ...@@ -57,6 +64,40 @@ class @Todos
$('.todos-pending .badge, .todos-pending-count').text data.count $('.todos-pending .badge, .todos-pending-count').text data.count
$('.todos-done .badge').text data.done_count $('.todos-done .badge').text data.done_count
getTotalPages: ->
@el.data('totalPages')
getCurrentPage: ->
@el.data('currentPage')
getTodosPerPage: ->
@el.data('perPage')
redirectIfNeeded: (total) ->
currPages = @getTotalPages()
currPage = @getCurrentPage()
# Refresh if no remaining Todos
if not total
location.reload()
return
# Do nothing if no pagination
return if not currPages
newPages = Math.ceil(total / @getTodosPerPage())
url = location.href # Includes query strings
# If new total of pages is different than we have now
if newPages isnt currPages
# Redirect to previous page if there's one available
if currPages > 1 and currPage is currPages
pageParams =
page: currPages - 1
url = gl.utils.mergeUrlParams(pageParams, url)
Turbolinks.visit(url)
goToTodoUrl: (e)-> goToTodoUrl: (e)->
todoLink = $(this).data('url') todoLink = $(this).data('url')
return unless todoLink return unless todoLink
......
...@@ -158,7 +158,7 @@ class @UsersSelect ...@@ -158,7 +158,7 @@ class @UsersSelect
if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex) if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex)
selectedId = user.id selectedId = user.id
Issues.filterResults $dropdown.closest('form') Issuable.filterResults $dropdown.closest('form')
else if $dropdown.hasClass 'js-filter-submit' else if $dropdown.hasClass 'js-filter-submit'
$dropdown.closest('form').submit() $dropdown.closest('form').submit()
else else
......
...@@ -248,7 +248,7 @@ ...@@ -248,7 +248,7 @@
.dropdown-title { .dropdown-title {
position: relative; position: relative;
padding: 0 25px 15px; padding: 0 25px 10px;
margin: 0 10px 10px; margin: 0 10px 10px;
font-weight: 600; font-weight: 600;
line-height: 1; line-height: 1;
...@@ -278,7 +278,7 @@ ...@@ -278,7 +278,7 @@
right: 5px; right: 5px;
width: 20px; width: 20px;
height: 20px; height: 20px;
top: -1px; top: -3px;
} }
.dropdown-menu-back { .dropdown-menu-back {
...@@ -320,7 +320,7 @@ ...@@ -320,7 +320,7 @@
} }
} }
.dropdown-input-field { .dropdown-input-field, .default-dropdown-input {
width: 100%; width: 100%;
padding: 0 7px; padding: 0 7px;
color: $dropdown-input-color; color: $dropdown-input-color;
...@@ -358,6 +358,13 @@ ...@@ -358,6 +358,13 @@
border-top: 1px solid $dropdown-divider-color; border-top: 1px solid $dropdown-divider-color;
} }
.dropdown-due-date-footer {
padding-top: 0;
margin-left: 10px;
margin-right: 10px;
border-top: 0;
}
.dropdown-footer-list { .dropdown-footer-list {
font-size: 14px; font-size: 14px;
...@@ -395,3 +402,122 @@ ...@@ -395,3 +402,122 @@
height: 15px; height: 15px;
border-radius: $border-radius-base; border-radius: $border-radius-base;
} }
.dropdown-menu-due-date {
.dropdown-content {
max-height: 230px;
}
.ui-widget {
table {
margin: 0;
}
&.ui-datepicker-inline {
padding: 0 10px;
border: 0;
width: 100%;
}
.ui-datepicker-header {
padding: 0 8px 10px;
border: 0;
.ui-icon {
background: none;
font-size: 20px;
text-indent: 0;
&:before {
display: block;
position: relative;
top: -2px;
color: $dropdown-title-btn-color;
font: normal normal normal 14px/1 FontAwesome;
font-size: inherit;
text-rendering: auto;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
}
}
.ui-state-active,
.ui-state-hover {
color: $md-link-color;
background-color: $calendar-hover-bg;
}
.ui-datepicker-prev,
.ui-datepicker-next {
top: 0;
height: 15px;
cursor: pointer;
&:hover {
background-color: transparent;
border: 0;
.ui-icon:before {
color: $md-link-color;
}
}
}
.ui-datepicker-prev {
left: 0;
.ui-icon:before {
content: '\f104';
text-align: left;
}
}
.ui-datepicker-next {
right: 0;
.ui-icon:before {
content: '\f105';
text-align: right;
}
}
td {
padding: 0;
border: 1px solid $calendar-border-color;
&:first-child {
border-left: 0;
}
&:last-child {
border-right: 0;
}
a {
line-height: 17px;
border: 0;
border-radius: 0;
}
}
.ui-datepicker-title {
color: $gl-gray;
font-size: 15px;
line-height: 1;
font-weight: normal;
}
}
th {
padding: 2px 0;
color: $calendar-header-color;
font-weight: normal;
text-transform: lowercase;
border-top: 1px solid $calendar-border-color;
}
.ui-datepicker-unselectable {
background-color: $calendar-unselectable-bg;
}
}
...@@ -38,12 +38,14 @@ ...@@ -38,12 +38,14 @@
.filename { .filename {
&.old { &.old {
display: inline-block;
span.idiff { span.idiff {
background-color: #f8cbcb; background-color: #f8cbcb;
} }
} }
&.new { &.new {
display: inline-block;
span.idiff { span.idiff {
background-color: #a6f3a6; background-color: #a6f3a6;
} }
......
...@@ -90,3 +90,12 @@ ...@@ -90,3 +90,12 @@
box-shadow: none; box-shadow: none;
width: 100%; width: 100%;
} }
.md {
&.md-preview-holder {
code {
white-space: pre-wrap;
word-break: break-all;
}
}
}
...@@ -241,3 +241,8 @@ $note-form-border-color: #e5e5e5; ...@@ -241,3 +241,8 @@ $note-form-border-color: #e5e5e5;
$note-toolbar-color: #959494; $note-toolbar-color: #959494;
$zen-control-hover-color: #111; $zen-control-hover-color: #111;
$calendar-header-color: #b8b8b8;
$calendar-hover-bg: #ecf3fe;
$calendar-border-color: rgba(#000, .1);
$calendar-unselectable-bg: #faf9f9;
...@@ -36,4 +36,10 @@ ...@@ -36,4 +36,10 @@
} }
} }
} }
.wiki {
code {
white-space: pre-wrap;
}
}
} }
...@@ -34,6 +34,7 @@ ...@@ -34,6 +34,7 @@
background: #fff; background: #fff;
color: #333; color: #333;
border-radius: 0 0 3px 3px; border-radius: 0 0 3px 3px;
-webkit-overflow-scrolling: auto;
.unfold { .unfold {
cursor: pointer; cursor: pointer;
...@@ -86,7 +87,7 @@ ...@@ -86,7 +87,7 @@
} }
span { span {
white-space: pre; white-space: pre-wrap;
} }
} }
} }
...@@ -335,7 +336,7 @@ ...@@ -335,7 +336,7 @@
} }
.diff-file .line_content { .diff-file .line_content {
white-space: pre; white-space: pre-wrap;
} }
.diff-wrap-lines .line_content { .diff-wrap-lines .line_content {
......
...@@ -26,6 +26,10 @@ ...@@ -26,6 +26,10 @@
line-height: 42px; line-height: 42px;
padding-top: 7px; padding-top: 7px;
padding-bottom: 7px; padding-bottom: 7px;
.pull-right {
height: 20px;
}
} }
.editor-ref { .editor-ref {
...@@ -53,4 +57,9 @@ ...@@ -53,4 +57,9 @@
.select2 { .select2 {
float: right; float: right;
} }
.encoding-selector,
.license-selector {
display: inline-block;
}
} }
...@@ -59,8 +59,10 @@ ...@@ -59,8 +59,10 @@
position: relative; position: relative;
overflow-y: auto; overflow-y: auto;
padding: 15px; padding: 15px;
.form-actions { .form-actions {
margin: -$gl-padding+1; margin: -$gl-padding+1;
margin-top: 15px;
} }
} }
......
...@@ -128,6 +128,7 @@ ...@@ -128,6 +128,7 @@
top: 58px; top: 58px;
bottom: 0; bottom: 0;
right: 0; right: 0;
z-index: 10;
transition: width .3s; transition: width .3s;
background: $gray-light; background: $gray-light;
padding: 10px 20px; padding: 10px 20px;
...@@ -241,16 +242,20 @@ ...@@ -241,16 +242,20 @@
} }
} }
.btn { .issuable-pager {
background: $gray-normal; background: $gray-normal;
border: 1px solid $border-gray-normal; border: 1px solid $border-gray-normal;
&:hover { &:hover {
background: $gray-dark; background: $gray-dark;
border: 1px solid $border-gray-dark; border: 1px solid $border-gray-dark;
} }
&.btn-primary {
@extend .btn-primary
}
} }
a:not(.btn) { a:not(.issuable-pager) {
&:hover { &:hover {
color: $md-link-color; color: $md-link-color;
text-decoration: none; text-decoration: none;
......
...@@ -42,6 +42,7 @@ ...@@ -42,6 +42,7 @@
.note-textarea { .note-textarea {
display: block; display: block;
padding: 10px 0; padding: 10px 0;
color: $gl-gray;
font-family: $regular_font; font-family: $regular_font;
border: 0; border: 0;
...@@ -83,18 +84,6 @@ ...@@ -83,18 +84,6 @@
border-color: $gl-success; border-color: $gl-success;
} }
} }
p {
code {
white-space: normal;
}
pre {
code {
white-space: pre;
}
}
}
} }
} }
......
...@@ -81,16 +81,8 @@ ul.notes { ...@@ -81,16 +81,8 @@ ul.notes {
@include md-typography; @include md-typography;
// On diffs code should wrap nicely and not overflow // On diffs code should wrap nicely and not overflow
p { code {
code { white-space: pre-wrap;
white-space: normal;
}
pre {
code {
white-space: pre;
}
}
} }
// Reset ul style types since we're nested inside a ul already // Reset ul style types since we're nested inside a ul already
...@@ -137,7 +129,7 @@ ul.notes { ...@@ -137,7 +129,7 @@ ul.notes {
margin-right: 10px; margin-right: 10px;
} }
.line_content { .line_content {
white-space: pre; white-space: pre-wrap;
} }
} }
...@@ -171,11 +163,6 @@ ul.notes { ...@@ -171,11 +163,6 @@ ul.notes {
&.parallel { &.parallel {
border-width: 1px; border-width: 1px;
.code,
code {
white-space: pre-wrap;
}
} }
.notes { .notes {
...@@ -196,6 +183,9 @@ ul.notes { ...@@ -196,6 +183,9 @@ ul.notes {
} }
} }
.author_link {
color: $gl-gray;
}
} }
.note-headline-light, .note-headline-light,
...@@ -308,7 +298,7 @@ ul.notes { ...@@ -308,7 +298,7 @@ ul.notes {
padding: 4px; padding: 4px;
font-size: 16px; font-size: 16px;
color: $gl-link-color; color: $gl-link-color;
margin-left: -60px; margin-left: -56px;
position: absolute; position: absolute;
z-index: 10; z-index: 10;
width: 32px; width: 32px;
......
...@@ -75,6 +75,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -75,6 +75,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:admin_notification_email, :admin_notification_email,
:user_oauth_applications, :user_oauth_applications,
:shared_runners_enabled, :shared_runners_enabled,
:shared_runners_text,
:max_artifacts_size, :max_artifacts_size,
:metrics_enabled, :metrics_enabled,
:metrics_host, :metrics_host,
...@@ -92,6 +93,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -92,6 +93,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:akismet_api_key, :akismet_api_key,
:email_author_in_body, :email_author_in_body,
:repository_checks_enabled, :repository_checks_enabled,
:metrics_packet_size,
restricted_visibility_levels: [], restricted_visibility_levels: [],
import_sources: [] import_sources: []
) )
......
...@@ -39,6 +39,6 @@ class Admin::HooksController < Admin::ApplicationController ...@@ -39,6 +39,6 @@ class Admin::HooksController < Admin::ApplicationController
end end
def hook_params def hook_params
params.require(:hook).permit(:url, :enable_ssl_verification) params.require(:hook).permit(:url, :enable_ssl_verification, :push_events, :tag_push_events)
end end
end end
class Projects::BuildsController < Projects::ApplicationController class Projects::BuildsController < Projects::ApplicationController
before_action :build, except: [:index, :cancel_all] before_action :build, except: [:index, :cancel_all]
before_action :authorize_read_build!, except: [:cancel, :cancel_all, :retry] before_action :authorize_read_build!, except: [:cancel, :cancel_all, :retry]
before_action :authorize_update_build!, except: [:index, :show, :status] before_action :authorize_update_build!, except: [:index, :show, :status, :raw]
layout 'project' layout 'project'
def index def index
...@@ -62,6 +62,14 @@ class Projects::BuildsController < Projects::ApplicationController ...@@ -62,6 +62,14 @@ class Projects::BuildsController < Projects::ApplicationController
notice: "Build has been sucessfully erased!" notice: "Build has been sucessfully erased!"
end end
def raw
if @build.has_trace?
send_file @build.path_to_trace, type: 'text/plain; charset=utf-8', disposition: 'inline'
else
render_404
end
end
private private
def build def build
......
...@@ -12,7 +12,7 @@ class Projects::CommitController < Projects::ApplicationController ...@@ -12,7 +12,7 @@ class Projects::CommitController < Projects::ApplicationController
before_action :authorize_read_commit_status!, only: [:builds] before_action :authorize_read_commit_status!, only: [:builds]
before_action :commit before_action :commit
before_action :define_show_vars, only: [:show, :builds] before_action :define_show_vars, only: [:show, :builds]
before_action :authorize_edit_tree!, only: [:revert] before_action :authorize_edit_tree!, only: [:revert, :cherry_pick]
def show def show
apply_diff_view_cookie! apply_diff_view_cookie!
...@@ -38,13 +38,13 @@ class Projects::CommitController < Projects::ApplicationController ...@@ -38,13 +38,13 @@ class Projects::CommitController < Projects::ApplicationController
end end
def cancel_builds def cancel_builds
ci_commit.builds.running_or_pending.each(&:cancel) ci_builds.running_or_pending.each(&:cancel)
redirect_back_or_default default: builds_namespace_project_commit_path(project.namespace, project, commit.sha) redirect_back_or_default default: builds_namespace_project_commit_path(project.namespace, project, commit.sha)
end end
def retry_builds def retry_builds
ci_commit.builds.latest.failed.each do |build| ci_builds.latest.failed.each do |build|
if build.retryable? if build.retryable?
Ci::Build.retry(build) Ci::Build.retry(build)
end end
...@@ -60,27 +60,32 @@ class Projects::CommitController < Projects::ApplicationController ...@@ -60,27 +60,32 @@ class Projects::CommitController < Projects::ApplicationController
end end
def revert def revert
assign_revert_commit_vars assign_change_commit_vars(@commit.revert_branch_name)
return render_404 if @target_branch.blank? return render_404 if @target_branch.blank?
create_commit(Commits::RevertService, success_notice: "The #{revert_type_title} has been successfully reverted.", create_commit(Commits::RevertService, success_notice: "The #{@commit.change_type_title} has been successfully reverted.",
success_path: successful_revert_path, failure_path: failed_revert_path) success_path: successful_change_path, failure_path: failed_change_path)
end end
def cherry_pick
assign_change_commit_vars(@commit.cherry_pick_branch_name)
return render_404 if @target_branch.blank?
private create_commit(Commits::CherryPickService, success_notice: "The #{@commit.change_type_title} has been successfully cherry-picked.",
success_path: successful_change_path, failure_path: failed_change_path)
def revert_type_title
@commit.merged_merge_request ? 'merge request' : 'commit'
end end
def successful_revert_path private
def successful_change_path
return referenced_merge_request_url if @commit.merged_merge_request return referenced_merge_request_url if @commit.merged_merge_request
namespace_project_commits_url(@project.namespace, @project, @target_branch) namespace_project_commits_url(@project.namespace, @project, @target_branch)
end end
def failed_revert_path def failed_change_path
return referenced_merge_request_url if @commit.merged_merge_request return referenced_merge_request_url if @commit.merged_merge_request
namespace_project_commit_url(@project.namespace, @project, params[:id]) namespace_project_commit_url(@project.namespace, @project, params[:id])
...@@ -94,8 +99,12 @@ class Projects::CommitController < Projects::ApplicationController ...@@ -94,8 +99,12 @@ class Projects::CommitController < Projects::ApplicationController
@commit ||= @project.commit(params[:id]) @commit ||= @project.commit(params[:id])
end end
def ci_commit def ci_commits
@ci_commit ||= project.ci_commit(commit.sha) @ci_commits ||= project.ci_commits.where(sha: commit.sha)
end
def ci_builds
@ci_builds ||= Ci::Build.where(commit: ci_commits)
end end
def define_show_vars def define_show_vars
...@@ -108,17 +117,17 @@ class Projects::CommitController < Projects::ApplicationController ...@@ -108,17 +117,17 @@ class Projects::CommitController < Projects::ApplicationController
@diff_refs = [commit.parent || commit, commit] @diff_refs = [commit.parent || commit, commit]
@notes_count = commit.notes.count @notes_count = commit.notes.count
@statuses = ci_commit.statuses if ci_commit @statuses = CommitStatus.where(commit: ci_commits)
@builds = Ci::Build.where(commit: ci_commits)
end end
def assign_revert_commit_vars def assign_change_commit_vars(mr_source_branch)
@commit = project.commit(params[:id]) @commit = project.commit(params[:id])
@target_branch = params[:target_branch] @target_branch = params[:target_branch]
@mr_source_branch = @commit.revert_branch_name @mr_source_branch = mr_source_branch
@mr_target_branch = @target_branch @mr_target_branch = @target_branch
@commit_params = { @commit_params = {
commit: @commit, commit: @commit,
revert_type_title: revert_type_title,
create_merge_request: params[:create_merge_request].present? || different_project? create_merge_request: params[:create_merge_request].present? || different_project?
} }
end end
......
...@@ -7,10 +7,12 @@ class Projects::GroupLinksController < Projects::ApplicationController ...@@ -7,10 +7,12 @@ class Projects::GroupLinksController < Projects::ApplicationController
end end
def create def create
link = project.project_group_links.new group = Group.find(params[:link_group_id])
link.group_id = params[:link_group_id] return render_404 unless can?(current_user, :read_group, group)
link.group_access = params[:link_group_access]
link.save project.project_group_links.create(
group: group, group_access: params[:link_group_access]
)
redirect_to namespace_project_group_links_path(project.namespace, project) redirect_to namespace_project_group_links_path(project.namespace, project)
end end
......
...@@ -33,14 +33,15 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -33,14 +33,15 @@ class Projects::IssuesController < Projects::ApplicationController
end end
@issues = @issues.page(params[:page]) @issues = @issues.page(params[:page])
@label = @project.labels.find_by(title: params[:label_name]) @labels = @project.labels.where(title: params[:label_name])
respond_to do |format| respond_to do |format|
format.html format.html
format.atom { render layout: false } format.atom { render layout: false }
format.json do format.json do
render json: { render json: {
html: view_to_html_string("projects/issues/_issues") html: view_to_html_string("projects/issues/_issues"),
labels: @labels.as_json(methods: :text_color)
} }
end end
end end
...@@ -128,10 +129,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -128,10 +129,7 @@ class Projects::IssuesController < Projects::ApplicationController
end end
def related_branches def related_branches
merge_requests = @issue.referenced_merge_requests(current_user) @related_branches = @issue.related_branches(current_user)
@related_branches = @issue.related_branches -
merge_requests.map(&:source_branch)
respond_to do |format| respond_to do |format|
format.json do format.json do
...@@ -194,7 +192,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -194,7 +192,7 @@ class Projects::IssuesController < Projects::ApplicationController
def issue_params def issue_params
params.require(:issue).permit( params.require(:issue).permit(
:title, :assignee_id, :position, :description, :confidential, :title, :assignee_id, :position, :description, :confidential,
:milestone_id, :state_event, :task_num, label_ids: [] :milestone_id, :due_date, :state_event, :task_num, label_ids: []
) )
end end
......
...@@ -38,13 +38,14 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -38,13 +38,14 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_requests = @merge_requests.page(params[:page]) @merge_requests = @merge_requests.page(params[:page])
@merge_requests = @merge_requests.preload(:target_project) @merge_requests = @merge_requests.preload(:target_project)
@label = @project.labels.find_by(title: params[:label_name]) @labels = @project.labels.where(title: params[:label_name])
respond_to do |format| respond_to do |format|
format.html format.html
format.json do format.json do
render json: { render json: {
html: view_to_html_string("projects/merge_requests/_merge_requests") html: view_to_html_string("projects/merge_requests/_merge_requests"),
labels: @labels.as_json(methods: :text_color)
} }
end end
end end
...@@ -320,6 +321,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -320,6 +321,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def define_widget_vars def define_widget_vars
@ci_commit = @merge_request.ci_commit @ci_commit = @merge_request.ci_commit
@ci_commits = [@ci_commit].compact
closes_issues closes_issues
end end
......
class Projects::ProjectMembersController < Projects::ApplicationController class Projects::ProjectMembersController < Projects::ApplicationController
# Authorize # Authorize
before_action :authorize_admin_project_member!, except: :leave before_action :authorize_admin_project_member!, except: [:leave, :index]
def index def index
@project_members = @project.project_members @project_members = @project.project_members
......
...@@ -6,7 +6,7 @@ class Projects::ServicesController < Projects::ApplicationController ...@@ -6,7 +6,7 @@ class Projects::ServicesController < Projects::ApplicationController
:description, :issues_url, :new_issue_url, :restrict_to_branch, :channel, :description, :issues_url, :new_issue_url, :restrict_to_branch, :channel,
:colorize_messages, :channels, :colorize_messages, :channels,
:push_events, :issues_events, :merge_requests_events, :tag_push_events, :push_events, :issues_events, :merge_requests_events, :tag_push_events,
:note_events, :build_events, :note_events, :build_events, :wiki_page_events,
:notify_only_broken_builds, :add_pusher, :notify_only_broken_builds, :add_pusher,
:send_from_committer_email, :disable_diffs, :external_wiki_url, :send_from_committer_email, :disable_diffs, :external_wiki_url,
:notify, :color, :notify, :color,
......
...@@ -44,7 +44,7 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -44,7 +44,7 @@ class Projects::WikisController < Projects::ApplicationController
return render('empty') unless can?(current_user, :create_wiki, @project) return render('empty') unless can?(current_user, :create_wiki, @project)
if @page.update(content, format, message) if @page = WikiPages::UpdateService.new(@project, current_user, wiki_params).execute(@page)
redirect_to( redirect_to(
namespace_project_wiki_path(@project.namespace, @project, @page), namespace_project_wiki_path(@project.namespace, @project, @page),
notice: 'Wiki was successfully updated.' notice: 'Wiki was successfully updated.'
...@@ -55,9 +55,9 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -55,9 +55,9 @@ class Projects::WikisController < Projects::ApplicationController
end end
def create def create
@page = WikiPage.new(@project_wiki) @page = WikiPages::CreateService.new(@project, current_user, wiki_params).execute
if @page.create(wiki_params) if @page.persisted?
redirect_to( redirect_to(
namespace_project_wiki_path(@project.namespace, @project, @page), namespace_project_wiki_path(@project.namespace, @project, @page),
notice: 'Wiki was successfully updated.' notice: 'Wiki was successfully updated.'
...@@ -122,15 +122,4 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -122,15 +122,4 @@ class Projects::WikisController < Projects::ApplicationController
params[:wiki].slice(:title, :content, :format, :message) params[:wiki].slice(:title, :content, :format, :message)
end end
def content
params[:wiki][:content]
end
def format
params[:wiki][:format]
end
def message
params[:wiki][:message]
end
end end
...@@ -39,6 +39,7 @@ class IssuableFinder ...@@ -39,6 +39,7 @@ class IssuableFinder
items = by_assignee(items) items = by_assignee(items)
items = by_author(items) items = by_author(items)
items = by_label(items) items = by_label(items)
items = by_due_date(items)
sort(items) sort(items)
end end
...@@ -117,7 +118,7 @@ class IssuableFinder ...@@ -117,7 +118,7 @@ class IssuableFinder
end end
def filter_by_no_label? def filter_by_no_label?
labels? && params[:label_name] == Label::None.title labels? && params[:label_name].include?(Label::None.title)
end end
def labels def labels
...@@ -271,18 +272,55 @@ class IssuableFinder ...@@ -271,18 +272,55 @@ class IssuableFinder
items = items.without_label items = items.without_label
else else
items = items.with_label(label_names) items = items.with_label(label_names)
if projects if projects
items = items.where(labels: { project_id: projects }) items = items.where(labels: { project_id: projects })
end end
end end
end end
# When filtering by multiple labels we may end up duplicating issues (if one
# has multiple labels). This ensures we only return unique issues.
items.distinct
end
def by_due_date(items)
if due_date?
if filter_by_no_due_date?
items = items.without_due_date
elsif filter_by_overdue?
items = items.due_before(Date.today)
elsif filter_by_due_this_week?
items = items.due_between(Date.today.beginning_of_week, Date.today.end_of_week)
elsif filter_by_due_this_month?
items = items.due_between(Date.today.beginning_of_month, Date.today.end_of_month)
end
end
items items
end end
def filter_by_no_due_date?
due_date? && params[:due_date] == Issue::NoDueDate.name
end
def filter_by_overdue?
due_date? && params[:due_date] == Issue::Overdue.name
end
def filter_by_due_this_week?
due_date? && params[:due_date] == Issue::DueThisWeek.name
end
def filter_by_due_this_month?
due_date? && params[:due_date] == Issue::DueThisMonth.name
end
def due_date?
params[:due_date].present? && klass.column_names.include?('due_date')
end
def label_names def label_names
params[:label_name].split(',') params[:label_name].is_a?(String) ? params[:label_name].split(',') : params[:label_name]
end end
def current_user_related? def current_user_related?
......
...@@ -254,11 +254,11 @@ module ApplicationHelper ...@@ -254,11 +254,11 @@ module ApplicationHelper
def page_filter_path(options = {}) def page_filter_path(options = {})
without = options.delete(:without) without = options.delete(:without)
add_label = options.delete(:label)
exist_opts = { exist_opts = {
state: params[:state], state: params[:state],
scope: params[:scope], scope: params[:scope],
label_name: params[:label_name],
milestone_title: params[:milestone_title], milestone_title: params[:milestone_title],
assignee_id: params[:assignee_id], assignee_id: params[:assignee_id],
author_id: params[:author_id], author_id: params[:author_id],
...@@ -275,6 +275,13 @@ module ApplicationHelper ...@@ -275,6 +275,13 @@ module ApplicationHelper
path = request.path path = request.path
path << "?#{options.to_param}" path << "?#{options.to_param}"
if add_label
if params[:label_name].present? and params[:label_name].respond_to?('any?')
params[:label_name].each do |label|
path << "&label_name[]=#{label}"
end
end
end
path path
end end
......
...@@ -15,6 +15,10 @@ module ApplicationSettingsHelper ...@@ -15,6 +15,10 @@ module ApplicationSettingsHelper
current_application_settings.sign_in_text current_application_settings.sign_in_text
end end
def shared_runners_text
current_application_settings.shared_runners_text
end
def user_oauth_applications? def user_oauth_applications?
current_application_settings.user_oauth_applications current_application_settings.user_oauth_applications
end end
......
...@@ -173,4 +173,15 @@ module BlobHelper ...@@ -173,4 +173,15 @@ module BlobHelper
response.etag = @blob.id response.etag = @blob.id
!stale !stale
end end
def licenses_for_select
return @licenses_for_select if defined?(@licenses_for_select)
licenses = Licensee::License.all
@licenses_for_select = {
Popular: licenses.select(&:featured).map { |license| [license.name, license.key] },
Other: licenses.reject(&:featured).map { |license| [license.name, license.key] }
}
end
end end
...@@ -4,14 +4,6 @@ module CiStatusHelper ...@@ -4,14 +4,6 @@ module CiStatusHelper
builds_namespace_project_commit_path(project.namespace, project, ci_commit.sha) builds_namespace_project_commit_path(project.namespace, project, ci_commit.sha)
end end
def ci_status_icon(ci_commit)
ci_icon_for_status(ci_commit.status)
end
def ci_status_label(ci_commit)
ci_label_for_status(ci_commit.status)
end
def ci_status_with_icon(status, target = nil) def ci_status_with_icon(status, target = nil)
content = ci_icon_for_status(status) + '&nbsp;'.html_safe + ci_label_for_status(status) content = ci_icon_for_status(status) + '&nbsp;'.html_safe + ci_label_for_status(status)
klass = "ci-status ci-#{status}" klass = "ci-status ci-#{status}"
...@@ -47,10 +39,13 @@ module CiStatusHelper ...@@ -47,10 +39,13 @@ module CiStatusHelper
end end
def render_ci_status(ci_commit, tooltip_placement: 'auto left') def render_ci_status(ci_commit, tooltip_placement: 'auto left')
link_to ci_status_icon(ci_commit), # TODO: split this method into
# - render_commit_status
# - render_pipeline_status
link_to ci_icon_for_status(ci_commit.status),
ci_status_path(ci_commit), ci_status_path(ci_commit),
class: "ci-status-link ci-status-icon-#{ci_commit.status.dasherize}", class: "ci-status-link ci-status-icon-#{ci_commit.status.dasherize}",
title: "Build #{ci_status_label(ci_commit)}", title: "Build #{ci_label_for_status(ci_commit.status)}",
data: { toggle: 'tooltip', placement: tooltip_placement } data: { toggle: 'tooltip', placement: tooltip_placement }
end end
......
...@@ -126,12 +126,10 @@ module CommitsHelper ...@@ -126,12 +126,10 @@ module CommitsHelper
def revert_commit_link(commit, continue_to_path, btn_class: nil) def revert_commit_link(commit, continue_to_path, btn_class: nil)
return unless current_user return unless current_user
tooltip = "Revert this #{revert_commit_type(commit)} in a new merge request" tooltip = "Revert this #{commit.change_type_title} in a new merge request"
if can_collaborate_with_project? if can_collaborate_with_project?
content_tag :span, 'data-toggle' => 'modal', 'data-target' => '#modal-revert-commit' do link_to 'Revert', '#modal-revert-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: tooltip, class: "btn btn-default btn-grouped btn-#{btn_class} has-tooltip"
link_to 'Revert', '#modal-revert-commit', 'data-toggle' => 'tooltip', 'data-container' => 'body', title: tooltip, class: "btn btn-default btn-grouped btn-#{btn_class}"
end
elsif can?(current_user, :fork_project, @project) elsif can?(current_user, :fork_project, @project)
continue_params = { continue_params = {
to: continue_to_path, to: continue_to_path,
...@@ -146,11 +144,24 @@ module CommitsHelper ...@@ -146,11 +144,24 @@ module CommitsHelper
end end
end end
def revert_commit_type(commit) def cherry_pick_commit_link(commit, continue_to_path, btn_class: nil)
if commit.merged_merge_request return unless current_user
'merge request'
else tooltip = "Cherry-pick this #{commit.change_type_title} in a new merge request"
'commit'
if can_collaborate_with_project?
link_to 'Cherry-pick', '#modal-cherry-pick-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: tooltip, class: "btn btn-default btn-grouped btn-#{btn_class} has-tooltip"
elsif can?(current_user, :fork_project, @project)
continue_params = {
to: continue_to_path,
notice: edit_in_new_fork_notice + ' Try to cherry-pick this commit again.',
notice_now: edit_in_new_fork_notice_now
}
fork_path = namespace_project_forks_path(@project.namespace, @project,
namespace_key: current_user.namespace.id,
continue: continue_params)
link_to 'Cherry-pick', fork_path, class: 'btn btn-grouped btn-close', method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: tooltip
end end
end end
...@@ -183,7 +194,7 @@ module CommitsHelper ...@@ -183,7 +194,7 @@ module CommitsHelper
options = { options = {
class: "commit-#{options[:source]}-link has-tooltip", class: "commit-#{options[:source]}-link has-tooltip",
data: { 'original-title'.to_sym => sanitize(source_email) } title: source_email
} }
if user.nil? if user.nil?
......
...@@ -25,6 +25,10 @@ module GitlabRoutingHelper ...@@ -25,6 +25,10 @@ module GitlabRoutingHelper
namespace_project_commits_path(project.namespace, project, @ref || project.repository.root_ref) namespace_project_commits_path(project.namespace, project, @ref || project.repository.root_ref)
end end
def project_pipelines_path(project, *args)
namespace_project_pipelines_path(project.namespace, project, *args)
end
def project_builds_path(project, *args) def project_builds_path(project, *args)
namespace_project_builds_path(project.namespace, project, *args) namespace_project_builds_path(project.namespace, project, *args)
end end
......
module ImportHelper
def github_project_link(path_with_namespace)
link_to path_with_namespace, github_project_url(path_with_namespace), target: '_blank'
end
private
def github_project_url(path_with_namespace)
"#{github_root_url}/#{path_with_namespace}"
end
def github_root_url
return @github_url if defined?(@github_url)
provider = Gitlab.config.omniauth.providers.find { |p| p.name == 'github' }
@github_url = provider.fetch('url', 'https://github.com') if provider
end
end
...@@ -16,6 +16,25 @@ module IssuablesHelper ...@@ -16,6 +16,25 @@ module IssuablesHelper
base_issuable_scope(issuable).where('iid > ?', issuable.iid).last base_issuable_scope(issuable).where('iid > ?', issuable.iid).last
end end
def multi_label_name(current_labels, default_label)
# current_labels may be a string from before
if current_labels.is_a?(Array)
if current_labels.count > 1
"#{current_labels[0]} +#{current_labels.count - 1} more"
else
current_labels[0]
end
elsif current_labels.is_a?(String)
if current_labels.nil? || current_labels.empty?
default_label
else
current_labels
end
else
default_label
end
end
def issuable_json_path(issuable) def issuable_json_path(issuable)
project = issuable.project project = issuable.project
......
...@@ -131,7 +131,7 @@ module IssuesHelper ...@@ -131,7 +131,7 @@ module IssuesHelper
class: "icon emoji-icon emoji-#{unicode}", class: "icon emoji-icon emoji-#{unicode}",
title: name, title: name,
data: data data: data
else else
# Emoji icons displayed separately, used for the awards already given # Emoji icons displayed separately, used for the awards already given
# to an issue or merge request. # to an issue or merge request.
content_tag :img, "", content_tag :img, "",
...@@ -172,6 +172,18 @@ module IssuesHelper ...@@ -172,6 +172,18 @@ module IssuesHelper
end.to_h end.to_h
end end
def due_date_options
options = [
Issue::AnyDueDate,
Issue::NoDueDate,
Issue::DueThisWeek,
Issue::DueThisMonth,
Issue::Overdue
]
options_from_collection_for_select(options, 'name', 'title', params[:due_date])
end
# Required for Banzai::Filter::IssueReferenceFilter # Required for Banzai::Filter::IssueReferenceFilter
module_function :url_for_issue module_function :url_for_issue
end end
...@@ -84,6 +84,14 @@ module PageLayoutHelper ...@@ -84,6 +84,14 @@ module PageLayoutHelper
end end
end end
def nav(name = nil)
if name
@nav = name
else
@nav
end
end
def fluid_layout(enabled = false) def fluid_layout(enabled = false)
if @fluid_layout.nil? if @fluid_layout.nil?
@fluid_layout = (current_user && current_user.layout == "fluid") || enabled @fluid_layout = (current_user && current_user.layout == "fluid") || enabled
......
...@@ -52,7 +52,7 @@ module ProjectsHelper ...@@ -52,7 +52,7 @@ module ProjectsHelper
link_to(author_html, user_path(author), class: "author_link #{"#{opts[:mobile_classes]}" if opts[:mobile_classes]}").html_safe link_to(author_html, user_path(author), class: "author_link #{"#{opts[:mobile_classes]}" if opts[:mobile_classes]}").html_safe
else else
title = opts[:title].sub(":name", sanitize(author.name)) title = opts[:title].sub(":name", sanitize(author.name))
link_to(author_html, user_path(author), class: "author_link has-tooltip", data: { 'original-title'.to_sym => title, container: 'body' } ).html_safe link_to(author_html, user_path(author), class: "author_link has-tooltip", title: title, data: { container: 'body' } ).html_safe
end end
end end
...@@ -144,6 +144,10 @@ module ProjectsHelper ...@@ -144,6 +144,10 @@ module ProjectsHelper
nav_tabs << :settings nav_tabs << :settings
end end
if can?(current_user, :read_project_member, project)
nav_tabs << :team
end
if can?(current_user, :read_issue, project) if can?(current_user, :read_issue, project)
nav_tabs << :issues nav_tabs << :issues
end end
...@@ -216,40 +220,14 @@ module ProjectsHelper ...@@ -216,40 +220,14 @@ module ProjectsHelper
end end
end end
def add_contribution_guide_path(project) def add_special_file_path(project, file_name:, commit_message: nil)
if project && !project.repository.contribution_guide namespace_project_new_blob_path(
namespace_project_new_blob_path( project.namespace,
project.namespace, project,
project, project.default_branch || 'master',
project.default_branch, file_name: file_name,
file_name: "CONTRIBUTING.md", commit_message: commit_message || "Add #{file_name.downcase}"
commit_message: "Add contribution guide" )
)
end
end
def add_changelog_path(project)
if project && !project.repository.changelog
namespace_project_new_blob_path(
project.namespace,
project,
project.default_branch,
file_name: "CHANGELOG",
commit_message: "Add changelog"
)
end
end
def add_license_path(project)
if project && !project.repository.license
namespace_project_new_blob_path(
project.namespace,
project,
project.default_branch,
file_name: "LICENSE",
commit_message: "Add license"
)
end
end end
def contribution_guide_path(project) def contribution_guide_path(project)
...@@ -272,7 +250,7 @@ module ProjectsHelper ...@@ -272,7 +250,7 @@ module ProjectsHelper
end end
def license_path(project) def license_path(project)
filename_path(project, :license) filename_path(project, :license_blob)
end end
def version_path(project) def version_path(project)
...@@ -306,6 +284,13 @@ module ProjectsHelper ...@@ -306,6 +284,13 @@ module ProjectsHelper
namespace_project_new_blob_path(@project.namespace, @project, tree_join(ref), file_name: 'README.md') namespace_project_new_blob_path(@project.namespace, @project, tree_join(ref), file_name: 'README.md')
end end
def new_license_path
ref = @repository.root_ref if @repository
ref ||= 'master'
namespace_project_new_blob_path(@project.namespace, @project, tree_join(ref), file_name: 'LICENSE')
end
def last_push_event def last_push_event
if current_user if current_user
current_user.recent_push(@project.id) current_user.recent_push(@project.id)
...@@ -335,6 +320,12 @@ module ProjectsHelper ...@@ -335,6 +320,12 @@ module ProjectsHelper
@ref || @repository.try(:root_ref) @ref || @repository.try(:root_ref)
end end
def license_short_name(project)
license = Licensee::License.new(project.repository.license_key)
license.nickname || license.name
end
private private
def filename_path(project, filename) def filename_path(project, filename)
......
...@@ -2,32 +2,29 @@ module SelectsHelper ...@@ -2,32 +2,29 @@ module SelectsHelper
def users_select_tag(id, opts = {}) def users_select_tag(id, opts = {})
css_class = "ajax-users-select " css_class = "ajax-users-select "
css_class << "multiselect " if opts[:multiple] css_class << "multiselect " if opts[:multiple]
css_class << "skip_ldap " if opts[:skip_ldap]
css_class << (opts[:class] || '') css_class << (opts[:class] || '')
value = opts[:selected] || '' value = opts[:selected] || ''
placeholder = opts[:placeholder] || 'Search for a user'
null_user = opts[:null_user] || false first_user = opts[:first_user] && current_user ? current_user.username : false
any_user = opts[:any_user] || false
email_user = opts[:email_user] || false
first_user = opts[:first_user] && current_user ? current_user.username : false
current_user = opts[:current_user] || false
author_id = opts[:author_id] || ''
project = opts[:project] || @project
html = { html = {
class: css_class, class: css_class,
data: { data: {
placeholder: placeholder, placeholder: opts[:placeholder] || 'Search for a user',
null_user: null_user, null_user: opts[:null_user] || false,
any_user: any_user, any_user: opts[:any_user] || false,
email_user: email_user, email_user: opts[:email_user] || false,
first_user: first_user, first_user: first_user,
current_user: current_user, current_user: opts[:current_user] || false,
author_id: author_id "push-code-to-protected-branches" => opts[:push_code_to_protected_branches],
author_id: opts[:author_id] || ''
} }
} }
unless opts[:scope] == :all unless opts[:scope] == :all
project = opts[:project] || @project
if project if project
html['data-project-id'] = project.id html['data-project-id'] = project.id
elsif @group elsif @group
......
...@@ -8,6 +8,8 @@ module SortingHelper ...@@ -8,6 +8,8 @@ module SortingHelper
sort_value_oldest_created => sort_title_oldest_created, sort_value_oldest_created => sort_title_oldest_created,
sort_value_milestone_soon => sort_title_milestone_soon, sort_value_milestone_soon => sort_title_milestone_soon,
sort_value_milestone_later => sort_title_milestone_later, sort_value_milestone_later => sort_title_milestone_later,
sort_value_due_date_soon => sort_title_due_date_soon,
sort_value_due_date_later => sort_title_due_date_later,
sort_value_largest_repo => sort_title_largest_repo, sort_value_largest_repo => sort_title_largest_repo,
sort_value_recently_signin => sort_title_recently_signin, sort_value_recently_signin => sort_title_recently_signin,
sort_value_oldest_signin => sort_title_oldest_signin, sort_value_oldest_signin => sort_title_oldest_signin,
...@@ -50,6 +52,14 @@ module SortingHelper ...@@ -50,6 +52,14 @@ module SortingHelper
'Milestone due later' 'Milestone due later'
end end
def sort_title_due_date_soon
'Due soon'
end
def sort_title_due_date_later
'Due later'
end
def sort_title_name def sort_title_name
'Name' 'Name'
end end
...@@ -98,6 +108,14 @@ module SortingHelper ...@@ -98,6 +108,14 @@ module SortingHelper
'milestone_due_desc' 'milestone_due_desc'
end end
def sort_value_due_date_soon
'due_date_asc'
end
def sort_value_due_date_later
'due_date_desc'
end
def sort_value_name def sort_value_name
'name_asc' 'name_asc'
end end
......
...@@ -110,4 +110,12 @@ module TabHelper ...@@ -110,4 +110,12 @@ module TabHelper
'active' 'active'
end end
end end
def profile_tab_class
if controller.controller_path =~ /\Aprofiles/
return 'active'
end
'active' if current_controller?('oauth/applications')
end
end end
...@@ -66,7 +66,7 @@ module TreeHelper ...@@ -66,7 +66,7 @@ module TreeHelper
ref ref
else else
project = tree_edit_project(project) project = tree_edit_project(project)
project.repository.next_patch_branch project.repository.next_branch('patch')
end end
end end
......
...@@ -56,7 +56,7 @@ module Emails ...@@ -56,7 +56,7 @@ module Emails
{ {
from: sender(sender_id), from: sender(sender_id),
to: recipient(recipient_id), to: recipient(recipient_id),
subject: subject("#{@merge_request.title} (##{@merge_request.iid})") subject: subject("#{@merge_request.title} (#{@merge_request.to_reference})")
} }
end end
end end
......
...@@ -38,7 +38,7 @@ module Emails ...@@ -38,7 +38,7 @@ module Emails
{ {
from: sender(@note.author_id), from: sender(@note.author_id),
to: recipient(recipient_id), to: recipient(recipient_id),
subject: subject("#{@note.noteable.title} (##{@note.noteable.iid})") subject: subject("#{@note.noteable.title} (#{@note.noteable.to_reference})")
} }
end end
......
...@@ -8,7 +8,7 @@ class RepositoryCheckMailer < BaseMailer ...@@ -8,7 +8,7 @@ class RepositoryCheckMailer < BaseMailer
mail( mail(
to: User.admins.pluck(:email), to: User.admins.pluck(:email),
subject: @message subject: "GitLab Admin | #{@message}"
) )
end end
end end
...@@ -37,8 +37,6 @@ ...@@ -37,8 +37,6 @@
module Ci module Ci
class Build < CommitStatus class Build < CommitStatus
LAZY_ATTRIBUTES = ['trace']
belongs_to :runner, class_name: 'Ci::Runner' belongs_to :runner, class_name: 'Ci::Runner'
belongs_to :trigger_request, class_name: 'Ci::TriggerRequest' belongs_to :trigger_request, class_name: 'Ci::TriggerRequest'
belongs_to :erased_by, class_name: 'User' belongs_to :erased_by, class_name: 'User'
...@@ -50,25 +48,17 @@ module Ci ...@@ -50,25 +48,17 @@ module Ci
scope :unstarted, ->() { where(runner_id: nil) } scope :unstarted, ->() { where(runner_id: nil) }
scope :ignore_failures, ->() { where(allow_failure: false) } scope :ignore_failures, ->() { where(allow_failure: false) }
scope :similar, ->(build) { where(ref: build.ref, tag: build.tag, trigger_request_id: build.trigger_request_id) }
mount_uploader :artifacts_file, ArtifactUploader mount_uploader :artifacts_file, ArtifactUploader
mount_uploader :artifacts_metadata, ArtifactUploader mount_uploader :artifacts_metadata, ArtifactUploader
acts_as_taggable acts_as_taggable
# To prevent db load megabytes of data from trace
default_scope -> { select(Ci::Build.columns_without_lazy) }
before_destroy { project } before_destroy { project }
class << self after_create :execute_hooks
def columns_without_lazy
(column_names - LAZY_ATTRIBUTES).map do |column_name|
"#{table_name}.#{column_name}"
end
end
class << self
def last_month def last_month
where('created_at > ?', Date.today - 1.month) where('created_at > ?', Date.today - 1.month)
end end
...@@ -126,12 +116,16 @@ module Ci ...@@ -126,12 +116,16 @@ module Ci
end end
def retried? def retried?
!self.commit.latest_statuses_for_ref(self.ref).include?(self) !self.commit.statuses.latest.include?(self)
end
def retry
Ci::Build.retry(self)
end end
def depends_on_builds def depends_on_builds
# Get builds of the same type # Get builds of the same type
latest_builds = self.commit.builds.similar(self).latest latest_builds = self.commit.builds.latest
# Return builds from previous stages # Return builds from previous stages
latest_builds.where('stage_idx < ?', stage_idx) latest_builds.where('stage_idx < ?', stage_idx)
...@@ -230,12 +224,33 @@ module Ci ...@@ -230,12 +224,33 @@ module Ci
end end
end end
def trace_length
if raw_trace
raw_trace.length
else
0
end
end
def trace=(trace) def trace=(trace)
recreate_trace_dir
File.write(path_to_trace, trace)
end
def recreate_trace_dir
unless Dir.exists?(dir_to_trace) unless Dir.exists?(dir_to_trace)
FileUtils.mkdir_p(dir_to_trace) FileUtils.mkdir_p(dir_to_trace)
end end
end
private :recreate_trace_dir
File.write(path_to_trace, trace) def append_trace(trace_part, offset)
recreate_trace_dir
File.truncate(path_to_trace, offset) if File.exist?(path_to_trace)
File.open(path_to_trace, 'a') do |f|
f.write(trace_part)
end
end end
def dir_to_trace def dir_to_trace
......
...@@ -19,21 +19,28 @@ ...@@ -19,21 +19,28 @@
module Ci module Ci
class Commit < ActiveRecord::Base class Commit < ActiveRecord::Base
extend Ci::Model extend Ci::Model
include Statuseable
belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id
has_many :statuses, class_name: 'CommitStatus' has_many :statuses, class_name: 'CommitStatus'
has_many :builds, class_name: 'Ci::Build' has_many :builds, class_name: 'Ci::Build'
has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest' has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest'
delegate :stages, to: :statuses
validates_presence_of :sha validates_presence_of :sha
validates_presence_of :status
validate :valid_commit_sha validate :valid_commit_sha
# Invalidate object and save if when touched
after_touch :update_state
def self.truncate_sha(sha) def self.truncate_sha(sha)
sha[0...8] sha[0...8]
end end
def to_param def self.stages
sha CommitStatus.where(commit: all).stages
end end
def project_id def project_id
...@@ -68,15 +75,20 @@ module Ci ...@@ -68,15 +75,20 @@ module Ci
nil nil
end end
def stage def branch?
running_or_pending = statuses.latest.running_or_pending.ordered !tag?
running_or_pending.first.try(:stage) end
def retryable?
builds.latest.any? do |build|
build.failed? && build.retryable?
end
end end
def create_builds(ref, tag, user, trigger_request = nil) def create_builds(user, trigger_request = nil)
return unless config_processor return unless config_processor
config_processor.stages.any? do |stage| config_processor.stages.any? do |stage|
CreateBuildsService.new.execute(self, stage, ref, tag, user, trigger_request, 'success').present? CreateBuildsService.new(self).execute(stage, user, 'success', trigger_request).present?
end end
end end
...@@ -84,7 +96,7 @@ module Ci ...@@ -84,7 +96,7 @@ module Ci
return unless config_processor return unless config_processor
# don't create other builds if this one is retried # don't create other builds if this one is retried
latest_builds = builds.similar(build).latest latest_builds = builds.latest
return unless latest_builds.exists?(build.id) return unless latest_builds.exists?(build.id)
# get list of stages after this build # get list of stages after this build
...@@ -92,88 +104,21 @@ module Ci ...@@ -92,88 +104,21 @@ module Ci
next_stages.delete(build.stage) next_stages.delete(build.stage)
# get status for all prior builds # get status for all prior builds
prior_builds = latest_builds.reject { |other_build| next_stages.include?(other_build.stage) } prior_builds = latest_builds.where.not(stage: next_stages)
status = Ci::Status.get_status(prior_builds) prior_status = prior_builds.status
# create builds for next stages based # create builds for next stages based
next_stages.any? do |stage| next_stages.any? do |stage|
CreateBuildsService.new.execute(self, stage, build.ref, build.tag, build.user, build.trigger_request, status).present? CreateBuildsService.new(self).execute(stage, build.user, prior_status, build.trigger_request).present?
end end
end end
def refs
statuses.order(:ref).pluck(:ref).uniq
end
def latest_statuses
@latest_statuses ||= statuses.latest.to_a
end
def latest_statuses_for_ref(ref)
latest_statuses.select { |status| status.ref == ref }
end
def matrix_builds(build = nil)
matrix_builds = builds.latest.ordered
matrix_builds = matrix_builds.similar(build) if build
matrix_builds.to_a
end
def retried def retried
@retried ||= (statuses.order(id: :desc) - statuses.latest) @retried ||= (statuses.order(id: :desc) - statuses.latest)
end end
def status
if yaml_errors.present?
return 'failed'
end
@status ||= Ci::Status.get_status(latest_statuses)
end
def pending?
status == 'pending'
end
def running?
status == 'running'
end
def success?
status == 'success'
end
def failed?
status == 'failed'
end
def canceled?
status == 'canceled'
end
def active?
running? || pending?
end
def complete?
canceled? || success? || failed?
end
def duration
duration_array = statuses.map(&:duration).compact
duration_array.reduce(:+).to_i
end
def started_at
@started_at ||= statuses.order('started_at ASC').first.try(:started_at)
end
def finished_at
@finished_at ||= statuses.order('finished_at DESC').first.try(:finished_at)
end
def coverage def coverage
coverage_array = latest_statuses.map(&:coverage).compact coverage_array = statuses.latest.map(&:coverage).compact
if coverage_array.size >= 1 if coverage_array.size >= 1
'%.2f' % (coverage_array.reduce(:+) / coverage_array.size) '%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
end end
...@@ -181,23 +126,29 @@ module Ci ...@@ -181,23 +126,29 @@ module Ci
def config_processor def config_processor
return nil unless ci_yaml_file return nil unless ci_yaml_file
@config_processor ||= Ci::GitlabCiYamlProcessor.new(ci_yaml_file, project.path_with_namespace) return @config_processor if defined?(@config_processor)
rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e
save_yaml_error(e.message) @config_processor ||= begin
nil Ci::GitlabCiYamlProcessor.new(ci_yaml_file, project.path_with_namespace)
rescue rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e
save_yaml_error("Undefined error") save_yaml_error(e.message)
nil nil
rescue
save_yaml_error("Undefined error")
nil
end
end end
def ci_yaml_file def ci_yaml_file
return @ci_yaml_file if defined?(@ci_yaml_file)
@ci_yaml_file ||= begin @ci_yaml_file ||= begin
blob = project.repository.blob_at(sha, '.gitlab-ci.yml') blob = project.repository.blob_at(sha, '.gitlab-ci.yml')
blob.load_all_data!(project.repository) blob.load_all_data!(project.repository)
blob.data blob.data
rescue
nil
end end
rescue
nil
end end
def skip_ci? def skip_ci?
...@@ -206,10 +157,23 @@ module Ci ...@@ -206,10 +157,23 @@ module Ci
private private
def update_state
statuses.reload
self.status = if yaml_errors.blank?
statuses.latest.status || 'skipped'
else
'failed'
end
self.started_at = statuses.started_at
self.finished_at = statuses.finished_at
self.duration = statuses.latest.duration
save
end
def save_yaml_error(error) def save_yaml_error(error)
return if self.yaml_errors? return if self.yaml_errors?
self.yaml_errors = error self.yaml_errors = error
save update_state
end end
end end
end end
...@@ -15,8 +15,8 @@ class Commit ...@@ -15,8 +15,8 @@ class Commit
DIFF_SAFE_LINES = Gitlab::Git::DiffCollection::DEFAULT_LIMITS[:max_lines] DIFF_SAFE_LINES = Gitlab::Git::DiffCollection::DEFAULT_LIMITS[:max_lines]
# Commits above this size will not be rendered in HTML # Commits above this size will not be rendered in HTML
DIFF_HARD_LIMIT_FILES = 1000 unless defined?(DIFF_HARD_LIMIT_FILES) DIFF_HARD_LIMIT_FILES = 1000
DIFF_HARD_LIMIT_LINES = 50000 unless defined?(DIFF_HARD_LIMIT_LINES) DIFF_HARD_LIMIT_LINES = 50000
class << self class << self
def decorate(commits, project) def decorate(commits, project)
...@@ -207,17 +207,22 @@ class Commit ...@@ -207,17 +207,22 @@ class Commit
@raw.short_id(7) @raw.short_id(7)
end end
def ci_commit def ci_commits
project.ci_commit(sha) @ci_commits ||= project.ci_commits.where(sha: sha)
end end
def status def status
ci_commit.try(:status) || :not_found return @status if defined?(@status)
@status ||= ci_commits.status
end end
def revert_branch_name def revert_branch_name
"revert-#{short_id}" "revert-#{short_id}"
end end
def cherry_pick_branch_name
project.repository.next_branch("cherry-pick-#{short_id}", mild: true)
end
def revert_description def revert_description
if merged_merge_request if merged_merge_request
...@@ -253,6 +258,10 @@ class Commit ...@@ -253,6 +258,10 @@ class Commit
end.any? { |commit_ref| commit_ref.reverts_commit?(self) } end.any? { |commit_ref| commit_ref.reverts_commit?(self) }
end end
def change_type_title
merged_merge_request ? 'merge request' : 'commit'
end
private private
def repo_changes def repo_changes
......
...@@ -33,30 +33,23 @@ ...@@ -33,30 +33,23 @@
# #
class CommitStatus < ActiveRecord::Base class CommitStatus < ActiveRecord::Base
include Statuseable
self.table_name = 'ci_builds' self.table_name = 'ci_builds'
belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id
belongs_to :commit, class_name: 'Ci::Commit' belongs_to :commit, class_name: 'Ci::Commit', touch: true
belongs_to :user belongs_to :user
validates :commit, presence: true validates :commit, presence: true
validates :status, inclusion: { in: %w(pending running failed success canceled) }
validates_presence_of :name validates_presence_of :name
alias_attribute :author, :user alias_attribute :author, :user
scope :running, -> { where(status: 'running') } scope :latest, -> { where(id: unscope(:select).select('max(id)').group(:name, :commit_id)) }
scope :pending, -> { where(status: 'pending') }
scope :success, -> { where(status: 'success') }
scope :failed, -> { where(status: 'failed') }
scope :running_or_pending, -> { where(status: [:running, :pending]) }
scope :finished, -> { where(status: [:success, :failed, :canceled]) }
scope :latest, -> { where(id: unscope(:select).select('max(id)').group(:name, :ref)) }
scope :ordered, -> { order(:ref, :stage_idx, :name) } scope :ordered, -> { order(:ref, :stage_idx, :name) }
scope :for_ref, ->(ref) { where(ref: ref) } scope :ignored, -> { where(allow_failure: true, status: [:failed, :canceled]) }
AVAILABLE_STATUSES = ['pending', 'running', 'success', 'failed', 'canceled']
state_machine :status, initial: :pending do state_machine :status, initial: :pending do
event :run do event :run do
...@@ -86,31 +79,24 @@ class CommitStatus < ActiveRecord::Base ...@@ -86,31 +79,24 @@ class CommitStatus < ActiveRecord::Base
after_transition [:pending, :running] => :success do |commit_status| after_transition [:pending, :running] => :success do |commit_status|
MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.commit.project, nil).trigger(commit_status) MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.commit.project, nil).trigger(commit_status)
end end
state :pending, value: 'pending'
state :running, value: 'running'
state :failed, value: 'failed'
state :success, value: 'success'
state :canceled, value: 'canceled'
end end
delegate :sha, :short_sha, to: :commit, prefix: false delegate :sha, :short_sha, to: :commit
# TODO: this should be removed with all references
def before_sha def before_sha
Gitlab::Git::BLANK_SHA commit.before_sha || Gitlab::Git::BLANK_SHA
end end
def started? def self.stages
!pending? && !canceled? && started_at order_by = 'max(stage_idx)'
group('stage').order(order_by).pluck(:stage, order_by).map(&:first).compact
end end
def active? def self.stages_status
running? || pending? all.stages.inject({}) do |h, stage|
end h[stage] = all.where(stage: stage).status
h
def complete? end
canceled? || success? || failed?
end end
def ignored? def ignored?
...@@ -118,11 +104,13 @@ class CommitStatus < ActiveRecord::Base ...@@ -118,11 +104,13 @@ class CommitStatus < ActiveRecord::Base
end end
def duration def duration
if started_at && finished_at duration =
finished_at - started_at if started_at && finished_at
elsif started_at finished_at - started_at
Time.now - started_at elsif started_at
end Time.now - started_at
end
duration
end end
def stuck? def stuck?
......
...@@ -37,7 +37,6 @@ module Issuable ...@@ -37,7 +37,6 @@ module Issuable
scope :closed, -> { with_state(:closed) } scope :closed, -> { with_state(:closed) }
scope :order_milestone_due_desc, -> { joins(:milestone).reorder('milestones.due_date DESC, milestones.id DESC') } scope :order_milestone_due_desc, -> { joins(:milestone).reorder('milestones.due_date DESC, milestones.id DESC') }
scope :order_milestone_due_asc, -> { joins(:milestone).reorder('milestones.due_date ASC, milestones.id ASC') } scope :order_milestone_due_asc, -> { joins(:milestone).reorder('milestones.due_date ASC, milestones.id ASC') }
scope :with_label, ->(title) { joins(:labels).where(labels: { title: title }) }
scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) } scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) }
scope :join_project, -> { joins(:project) } scope :join_project, -> { joins(:project) }
...@@ -122,6 +121,14 @@ module Issuable ...@@ -122,6 +121,14 @@ module Issuable
joins(join_clause).group(issuable_table[:id]).reorder("COUNT(notes.id) DESC") joins(join_clause).group(issuable_table[:id]).reorder("COUNT(notes.id) DESC")
end end
def with_label(title)
if title.is_a?(Array) && title.count > 1
joins(:labels).where(labels: { title: title }).group('issues.id').having("count(distinct labels.title) = #{title.count}")
else
joins(:labels).where(labels: { title: title })
end
end
end end
def today? def today?
......
module Statuseable
extend ActiveSupport::Concern
AVAILABLE_STATUSES = %w(pending running success failed canceled skipped)
class_methods do
def status_sql
builds = all.select('count(*)').to_sql
success = all.success.select('count(*)').to_sql
ignored = all.ignored.select('count(*)').to_sql if all.respond_to?(:ignored)
ignored ||= '0'
pending = all.pending.select('count(*)').to_sql
running = all.running.select('count(*)').to_sql
canceled = all.canceled.select('count(*)').to_sql
skipped = all.skipped.select('count(*)').to_sql
deduce_status = "(CASE
WHEN (#{builds})=0 THEN NULL
WHEN (#{builds})=(#{success})+(#{ignored}) THEN 'success'
WHEN (#{builds})=(#{pending}) THEN 'pending'
WHEN (#{builds})=(#{canceled}) THEN 'canceled'
WHEN (#{builds})=(#{skipped}) THEN 'skipped'
WHEN (#{running})+(#{pending})>0 THEN 'running'
ELSE 'failed'
END)"
deduce_status
end
def status
all.pluck(self.status_sql).first
end
def duration
duration_array = all.map(&:duration).compact
duration_array.reduce(:+)
end
def started_at
all.minimum(:started_at)
end
def finished_at
all.maximum(:finished_at)
end
end
included do
validates :status, inclusion: { in: AVAILABLE_STATUSES }
state_machine :status, initial: :pending do
state :pending, value: 'pending'
state :running, value: 'running'
state :failed, value: 'failed'
state :success, value: 'success'
state :canceled, value: 'canceled'
state :skipped, value: 'skipped'
end
scope :running, -> { where(status: 'running') }
scope :pending, -> { where(status: 'pending') }
scope :success, -> { where(status: 'success') }
scope :failed, -> { where(status: 'failed') }
scope :canceled, -> { where(status: 'canceled') }
scope :skipped, -> { where(status: 'skipped') }
scope :running_or_pending, -> { where(status: [:running, :pending]) }
scope :finished, -> { where(status: [:success, :failed, :canceled]) }
end
def started?
!pending? && !canceled? && started_at
end
def active?
running? || pending?
end
def complete?
canceled? || success? || failed?
end
end
...@@ -15,7 +15,6 @@ ...@@ -15,7 +15,6 @@
# #
require 'carrierwave/orm/activerecord' require 'carrierwave/orm/activerecord'
require 'file_size_validator'
class Group < Namespace class Group < Namespace
include Gitlab::ConfigHelper include Gitlab::ConfigHelper
......
...@@ -21,10 +21,9 @@ ...@@ -21,10 +21,9 @@
class ProjectHook < WebHook class ProjectHook < WebHook
belongs_to :project belongs_to :project
scope :push_hooks, -> { where(push_events: true) }
scope :tag_push_hooks, -> { where(tag_push_events: true) }
scope :issue_hooks, -> { where(issues_events: true) } scope :issue_hooks, -> { where(issues_events: true) }
scope :note_hooks, -> { where(note_events: true) } scope :note_hooks, -> { where(note_events: true) }
scope :merge_request_hooks, -> { where(merge_requests_events: true) } scope :merge_request_hooks, -> { where(merge_requests_events: true) }
scope :build_hooks, -> { where(build_events: true) } scope :build_hooks, -> { where(build_events: true) }
scope :wiki_page_hooks, -> { where(wiki_page_events: true) }
end end
...@@ -19,4 +19,7 @@ ...@@ -19,4 +19,7 @@
# #
class SystemHook < WebHook class SystemHook < WebHook
def async_execute(data, hook_name)
Sidekiq::Client.enqueue(SystemHookWorker, id, data, hook_name)
end
end end
...@@ -30,6 +30,9 @@ class WebHook < ActiveRecord::Base ...@@ -30,6 +30,9 @@ class WebHook < ActiveRecord::Base
default_value_for :build_events, false default_value_for :build_events, false
default_value_for :enable_ssl_verification, true default_value_for :enable_ssl_verification, true
scope :push_hooks, -> { where(push_events: true) }
scope :tag_push_hooks, -> { where(tag_push_events: true) }
# HTTParty timeout # HTTParty timeout
default_timeout Gitlab.config.gitlab.webhook_timeout default_timeout Gitlab.config.gitlab.webhook_timeout
......
...@@ -20,7 +20,6 @@ ...@@ -20,7 +20,6 @@
# #
require 'carrierwave/orm/activerecord' require 'carrierwave/orm/activerecord'
require 'file_size_validator'
class Issue < ActiveRecord::Base class Issue < ActiveRecord::Base
include InternalId include InternalId
...@@ -29,6 +28,13 @@ class Issue < ActiveRecord::Base ...@@ -29,6 +28,13 @@ class Issue < ActiveRecord::Base
include Sortable include Sortable
include Taskable include Taskable
DueDateStruct = Struct.new(:title, :name).freeze
NoDueDate = DueDateStruct.new('No Due Date', '0').freeze
AnyDueDate = DueDateStruct.new('Any Due Date', '').freeze
Overdue = DueDateStruct.new('Overdue', 'overdue').freeze
DueThisWeek = DueDateStruct.new('Due This Week', 'week').freeze
DueThisMonth = DueDateStruct.new('Due This Month', 'month').freeze
ActsAsTaggableOn.strict_case_match = true ActsAsTaggableOn.strict_case_match = true
belongs_to :project belongs_to :project
...@@ -40,6 +46,13 @@ class Issue < ActiveRecord::Base ...@@ -40,6 +46,13 @@ class Issue < ActiveRecord::Base
scope :open_for, ->(user) { opened.assigned_to(user) } scope :open_for, ->(user) { opened.assigned_to(user) }
scope :in_projects, ->(project_ids) { where(project_id: project_ids) } scope :in_projects, ->(project_ids) { where(project_id: project_ids) }
scope :without_due_date, -> { where(due_date: nil) }
scope :due_before, ->(date) { where('issues.due_date < ?', date) }
scope :due_between, ->(from_date, to_date) { where('issues.due_date >= ?', from_date).where('issues.due_date <= ?', to_date) }
scope :order_due_date_asc, -> { reorder('issues.due_date IS NULL, issues.due_date ASC') }
scope :order_due_date_desc, -> { reorder('issues.due_date IS NULL, issues.due_date DESC') }
state_machine :state, initial: :opened do state_machine :state, initial: :opened do
event :close do event :close do
transition [:reopened, :opened] => :closed transition [:reopened, :opened] => :closed
...@@ -83,6 +96,15 @@ class Issue < ActiveRecord::Base ...@@ -83,6 +96,15 @@ class Issue < ActiveRecord::Base
@link_reference_pattern ||= super("issues", /(?<issue>\d+)/) @link_reference_pattern ||= super("issues", /(?<issue>\d+)/)
end end
def self.sort(method)
case method.to_s
when 'due_date_asc' then order_due_date_asc
when 'due_date_desc' then order_due_date_desc
else
super
end
end
def to_reference(from_project = nil) def to_reference(from_project = nil)
reference = "#{self.class.reference_prefix}#{iid}" reference = "#{self.class.reference_prefix}#{iid}"
...@@ -104,10 +126,16 @@ class Issue < ActiveRecord::Base ...@@ -104,10 +126,16 @@ class Issue < ActiveRecord::Base
end end
end end
def related_branches # All branches containing the current issue's ID, except for
project.repository.branch_names.select do |branch| # those with a merge request open referencing the current issue.
def related_branches(current_user)
branches_with_iid = project.repository.branch_names.select do |branch|
branch =~ /\A#{iid}-(?!\d+-stable)/i branch =~ /\A#{iid}-(?!\d+-stable)/i
end end
branches_with_merge_request = self.referenced_merge_requests(current_user).map(&:source_branch)
branches_with_iid - branches_with_merge_request
end end
# Reset issue events cache # Reset issue events cache
...@@ -151,13 +179,21 @@ class Issue < ActiveRecord::Base ...@@ -151,13 +179,21 @@ class Issue < ActiveRecord::Base
end end
def to_branch_name def to_branch_name
"#{iid}-#{title.parameterize}" if self.confidential?
"#{iid}-confidential-issue"
else
"#{iid}-#{title.parameterize}"
end
end end
def can_be_worked_on?(current_user) def can_be_worked_on?(current_user)
!self.closed? && !self.closed? &&
!self.project.forked? && !self.project.forked? &&
self.related_branches.empty? && self.related_branches(current_user).empty? &&
self.closed_by_merge_requests(current_user).empty? self.closed_by_merge_requests(current_user).empty?
end end
def overdue?
due_date.try(:past?) || false
end
end end
...@@ -113,6 +113,10 @@ class Label < ActiveRecord::Base ...@@ -113,6 +113,10 @@ class Label < ActiveRecord::Base
template template
end end
def text_color
LabelsHelper::text_color_for_bg(self.color)
end
private private
def label_format_reference(format = :id) def label_format_reference(format = :id)
......
...@@ -27,9 +27,6 @@ ...@@ -27,9 +27,6 @@
# merge_commit_sha :string # merge_commit_sha :string
# #
require Rails.root.join("app/models/commit")
require Rails.root.join("lib/static_model")
class MergeRequest < ActiveRecord::Base class MergeRequest < ActiveRecord::Base
include InternalId include InternalId
include Issuable include Issuable
...@@ -589,7 +586,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -589,7 +586,7 @@ class MergeRequest < ActiveRecord::Base
end end
def ci_commit def ci_commit
@ci_commit ||= source_project.ci_commit(last_commit.id) if last_commit && source_project @ci_commit ||= source_project.ci_commit(last_commit.id, source_branch) if last_commit && source_project
end end
def diff_refs def diff_refs
...@@ -605,4 +602,8 @@ class MergeRequest < ActiveRecord::Base ...@@ -605,4 +602,8 @@ class MergeRequest < ActiveRecord::Base
def can_be_reverted?(current_user = nil) def can_be_reverted?(current_user = nil)
merge_commit && !merge_commit.has_been_reverted?(current_user, self) merge_commit && !merge_commit.has_been_reverted?(current_user, self)
end end
def can_be_cherry_picked?
merge_commit
end
end end
...@@ -11,8 +11,6 @@ ...@@ -11,8 +11,6 @@
# updated_at :datetime # updated_at :datetime
# #
require Rails.root.join("app/models/commit")
class MergeRequestDiff < ActiveRecord::Base class MergeRequestDiff < ActiveRecord::Base
include Sortable include Sortable
......
...@@ -20,7 +20,6 @@ ...@@ -20,7 +20,6 @@
# #
require 'carrierwave/orm/activerecord' require 'carrierwave/orm/activerecord'
require 'file_size_validator'
class Note < ActiveRecord::Base class Note < ActiveRecord::Base
include Gitlab::CurrentSettings include Gitlab::CurrentSettings
......
...@@ -40,7 +40,6 @@ ...@@ -40,7 +40,6 @@
# #
require 'carrierwave/orm/activerecord' require 'carrierwave/orm/activerecord'
require 'file_size_validator'
class Project < ActiveRecord::Base class Project < ActiveRecord::Base
include Gitlab::ConfigHelper include Gitlab::ConfigHelper
...@@ -831,8 +830,8 @@ class Project < ActiveRecord::Base ...@@ -831,8 +830,8 @@ class Project < ActiveRecord::Base
end end
end end
def hook_attrs def hook_attrs(backward: true)
{ attrs = {
name: name, name: name,
description: description, description: description,
web_url: web_url, web_url: web_url,
...@@ -843,12 +842,19 @@ class Project < ActiveRecord::Base ...@@ -843,12 +842,19 @@ class Project < ActiveRecord::Base
visibility_level: visibility_level, visibility_level: visibility_level,
path_with_namespace: path_with_namespace, path_with_namespace: path_with_namespace,
default_branch: default_branch, default_branch: default_branch,
# Backward compatibility
homepage: web_url,
url: url_to_repo,
ssh_url: ssh_url_to_repo,
http_url: http_url_to_repo
} }
# Backward compatibility
if backward
attrs.merge!({
homepage: web_url,
url: url_to_repo,
ssh_url: ssh_url_to_repo,
http_url: http_url_to_repo
})
end
attrs
end end
# Reset events cache related to this project # Reset events cache related to this project
...@@ -957,12 +963,12 @@ class Project < ActiveRecord::Base ...@@ -957,12 +963,12 @@ class Project < ActiveRecord::Base
!namespace.share_with_group_lock !namespace.share_with_group_lock
end end
def ci_commit(sha) def ci_commit(sha, ref)
ci_commits.find_by(sha: sha) ci_commits.order(id: :desc).find_by(sha: sha, ref: ref)
end end
def ensure_ci_commit(sha) def ensure_ci_commit(sha, ref)
ci_commit(sha) || ci_commits.create(sha: sha) ci_commit(sha, ref) || ci_commits.create(sha: sha, ref: ref)
end end
def enable_ci def enable_ci
......
...@@ -8,7 +8,6 @@ ...@@ -8,7 +8,6 @@
# #
require 'carrierwave/orm/activerecord' require 'carrierwave/orm/activerecord'
require 'file_size_validator'
class ProjectImportData < ActiveRecord::Base class ProjectImportData < ActiveRecord::Base
belongs_to :project belongs_to :project
......
...@@ -183,7 +183,7 @@ class HipchatService < Service ...@@ -183,7 +183,7 @@ class HipchatService < Service
title = obj_attr[:title] title = obj_attr[:title]
merge_request_url = "#{project_url}/merge_requests/#{merge_request_id}" merge_request_url = "#{project_url}/merge_requests/#{merge_request_id}"
merge_request_link = "<a href=\"#{merge_request_url}\">merge request ##{merge_request_id}</a>" merge_request_link = "<a href=\"#{merge_request_url}\">merge request !#{merge_request_id}</a>"
message = "#{user_name} #{state} #{merge_request_link} in " \ message = "#{user_name} #{state} #{merge_request_link} in " \
"#{project_link}: <b>#{title}</b>" "#{project_link}: <b>#{title}</b>"
...@@ -224,7 +224,7 @@ class HipchatService < Service ...@@ -224,7 +224,7 @@ class HipchatService < Service
when "MergeRequest" when "MergeRequest"
subj_attr = HashWithIndifferentAccess.new(data[:merge_request]) subj_attr = HashWithIndifferentAccess.new(data[:merge_request])
subject_id = subj_attr[:iid] subject_id = subj_attr[:iid]
subject_desc = "##{subject_id}" subject_desc = "!#{subject_id}"
subject_type = "merge request" subject_type = "merge request"
title = format_title(subj_attr[:title]) title = format_title(subj_attr[:title])
when "Snippet" when "Snippet"
......
...@@ -60,7 +60,7 @@ class SlackService < Service ...@@ -60,7 +60,7 @@ class SlackService < Service
end end
def supported_events def supported_events
%w(push issue merge_request note tag_push build) %w(push issue merge_request note tag_push build wiki_page)
end end
def execute(data) def execute(data)
...@@ -90,6 +90,8 @@ class SlackService < Service ...@@ -90,6 +90,8 @@ class SlackService < Service
NoteMessage.new(data) NoteMessage.new(data)
when "build" when "build"
BuildMessage.new(data) if should_build_be_notified?(data) BuildMessage.new(data) if should_build_be_notified?(data)
when "wiki_page"
WikiPageMessage.new(data)
end end
opt = {} opt = {}
...@@ -133,3 +135,4 @@ require "slack_service/push_message" ...@@ -133,3 +135,4 @@ require "slack_service/push_message"
require "slack_service/merge_message" require "slack_service/merge_message"
require "slack_service/note_message" require "slack_service/note_message"
require "slack_service/build_message" require "slack_service/build_message"
require "slack_service/wiki_page_message"
...@@ -50,7 +50,7 @@ class SlackService ...@@ -50,7 +50,7 @@ class SlackService
end end
def merge_request_link def merge_request_link
"[merge request ##{merge_request_id}](#{merge_request_url})" "[merge request !#{merge_request_id}](#{merge_request_url})"
end end
def merge_request_url def merge_request_url
......
...@@ -58,7 +58,7 @@ class SlackService ...@@ -58,7 +58,7 @@ class SlackService
def create_merge_note(merge_request) def create_merge_note(merge_request)
commented_on_message( commented_on_message(
"[merge request ##{merge_request[:iid]}](#{@note_url})", "[merge request !#{merge_request[:iid]}](#{@note_url})",
format_title(merge_request[:title])) format_title(merge_request[:title]))
end end
......
class SlackService
class WikiPageMessage < BaseMessage
attr_reader :user_name
attr_reader :title
attr_reader :project_name
attr_reader :project_url
attr_reader :wiki_page_url
attr_reader :action
attr_reader :description
def initialize(params)
@user_name = params[:user][:name]
@project_name = params[:project_name]
@project_url = params[:project_url]
obj_attr = params[:object_attributes]
obj_attr = HashWithIndifferentAccess.new(obj_attr)
@title = obj_attr[:title]
@wiki_page_url = obj_attr[:url]
@description = obj_attr[:content]
@action =
case obj_attr[:action]
when "create"
"created"
when "update"
"edited"
end
end
def attachments
description_message
end
private
def message
"#{user_name} #{action} #{wiki_page_link} in #{project_link}: *#{title}*"
end
def description_message
[{ text: format(@description), color: attachment_color }]
end
def project_link
"[#{project_name}](#{project_url})"
end
def wiki_page_link
"[wiki page](#{wiki_page_url})"
end
end
end
...@@ -228,7 +228,8 @@ class Repository ...@@ -228,7 +228,8 @@ class Repository
def cache_keys def cache_keys
%i(size branch_names tag_names commit_count %i(size branch_names tag_names commit_count
readme version contribution_guide changelog license) readme version contribution_guide changelog
license_blob license_key)
end end
def build_cache def build_cache
...@@ -461,27 +462,21 @@ class Repository ...@@ -461,27 +462,21 @@ class Repository
end end
end end
def license def license_blob
cache.fetch(:license) do return nil if !exists? || empty?
licenses = tree(:head).blobs.find_all do |file|
file.name =~ /\A(copying|license|licence)/i
end
preferences = [
/\Alicen[sc]e\z/i, # LICENSE, LICENCE
/\Alicen[sc]e\./i, # LICENSE.md, LICENSE.txt
/\Acopying\z/i, # COPYING
/\Acopying\.(?!lesser)/i, # COPYING.txt
/Acopying.lesser/i # COPYING.LESSER
]
license = nil cache.fetch(:license_blob) do
preferences.each do |r| if licensee_project.license
license = licenses.find { |l| l.name =~ r } blob_at_branch(root_ref, licensee_project.matched_file.filename)
break if license
end end
end
end
license def license_key
return nil if !exists? || empty?
cache.fetch(:license_key) do
licensee_project.license.try(:key) || 'no-license'
end end
end end
...@@ -549,15 +544,18 @@ class Repository ...@@ -549,15 +544,18 @@ class Repository
commit(sha) commit(sha)
end end
def next_patch_branch def next_branch(name, opts={})
patch_branch_ids = self.branch_names.map do |n| branch_ids = self.branch_names.map do |n|
result = n.match(/\Apatch-([0-9]+)\z/) next 1 if n == name
result = n.match(/\A#{name}-([0-9]+)\z/)
result[1].to_i if result result[1].to_i if result
end.compact end.compact
highest_patch_branch_id = patch_branch_ids.max || 0 highest_branch_id = branch_ids.max || 0
"patch-#{highest_patch_branch_id + 1}" return name if opts[:mild] && 0 == highest_branch_id
"#{name}-#{highest_branch_id + 1}"
end end
# Remove archives older than 2 hours # Remove archives older than 2 hours
...@@ -760,6 +758,28 @@ class Repository ...@@ -760,6 +758,28 @@ class Repository
end end
end end
def cherry_pick(user, commit, base_branch, cherry_pick_tree_id = nil)
source_sha = find_branch(base_branch).target
cherry_pick_tree_id ||= check_cherry_pick_content(commit, base_branch)
return false unless cherry_pick_tree_id
commit_with_hooks(user, base_branch) do |ref|
committer = user_to_committer(user)
source_sha = Rugged::Commit.create(rugged,
message: commit.message,
author: {
email: commit.author_email,
name: commit.author_name,
time: commit.authored_date
},
committer: committer,
tree: cherry_pick_tree_id,
parents: [rugged.lookup(source_sha)],
update_ref: ref)
end
end
def check_revert_content(commit, base_branch) def check_revert_content(commit, base_branch)
source_sha = find_branch(base_branch).target source_sha = find_branch(base_branch).target
args = [commit.id, source_sha] args = [commit.id, source_sha]
...@@ -774,6 +794,20 @@ class Repository ...@@ -774,6 +794,20 @@ class Repository
tree_id tree_id
end end
def check_cherry_pick_content(commit, base_branch)
source_sha = find_branch(base_branch).target
args = [commit.id, source_sha]
args << 1 if commit.merge_commit?
cherry_pick_index = rugged.cherrypick_commit(*args)
return false if cherry_pick_index.conflicts?
tree_id = cherry_pick_index.write_tree(rugged)
return false unless diff_exists?(source_sha, tree_id)
tree_id
end
def diff_exists?(sha1, sha2) def diff_exists?(sha1, sha2)
rugged.diff(sha1, sha2).size > 0 rugged.diff(sha1, sha2).size > 0
end end
...@@ -925,4 +959,8 @@ class Repository ...@@ -925,4 +959,8 @@ class Repository
def cache def cache
@cache ||= RepositoryCache.new(path_with_namespace) @cache ||= RepositoryCache.new(path_with_namespace)
end end
def licensee_project
@licensee_project ||= Licensee.project(path)
end
end end
...@@ -32,6 +32,7 @@ class Service < ActiveRecord::Base ...@@ -32,6 +32,7 @@ class Service < ActiveRecord::Base
default_value_for :tag_push_events, true default_value_for :tag_push_events, true
default_value_for :note_events, true default_value_for :note_events, true
default_value_for :build_events, true default_value_for :build_events, true
default_value_for :wiki_page_events, true
after_initialize :initialize_properties after_initialize :initialize_properties
...@@ -53,6 +54,7 @@ class Service < ActiveRecord::Base ...@@ -53,6 +54,7 @@ class Service < ActiveRecord::Base
scope :merge_request_hooks, -> { where(merge_requests_events: true, active: true) } scope :merge_request_hooks, -> { where(merge_requests_events: true, active: true) }
scope :note_hooks, -> { where(note_events: true, active: true) } scope :note_hooks, -> { where(note_events: true, active: true) }
scope :build_hooks, -> { where(build_events: true, active: true) } scope :build_hooks, -> { where(build_events: true, active: true) }
scope :wiki_page_hooks, -> { where(wiki_page_events: true, active: true) }
default_value_for :category, 'common' default_value_for :category, 'common'
...@@ -94,7 +96,7 @@ class Service < ActiveRecord::Base ...@@ -94,7 +96,7 @@ class Service < ActiveRecord::Base
end end
def supported_events def supported_events
%w(push tag_push issue merge_request) %w(push tag_push issue merge_request wiki_page)
end end
def execute(data) def execute(data)
......
...@@ -63,7 +63,6 @@ ...@@ -63,7 +63,6 @@
# #
require 'carrierwave/orm/activerecord' require 'carrierwave/orm/activerecord'
require 'file_size_validator'
class User < ActiveRecord::Base class User < ActiveRecord::Base
extend Gitlab::ConfigHelper extend Gitlab::ConfigHelper
......
...@@ -29,6 +29,10 @@ class WikiPage ...@@ -29,6 +29,10 @@ class WikiPage
# new Page values before writing to the Gollum repository. # new Page values before writing to the Gollum repository.
attr_accessor :attributes attr_accessor :attributes
def hook_attrs
attributes
end
def initialize(wiki, page = nil, persisted = false) def initialize(wiki, page = nil, persisted = false)
@wiki = wiki @wiki = wiki
@page = page @page = page
......
module Ci module Ci
class CreateBuildsService class CreateBuildsService
def execute(commit, stage, ref, tag, user, trigger_request, status) def initialize(commit)
builds_attrs = commit.config_processor.builds_for_stage_and_ref(stage, ref, tag, trigger_request) @commit = commit
end
def execute(stage, user, status, trigger_request = nil)
builds_attrs = config_processor.builds_for_stage_and_ref(stage, @commit.ref, @commit.tag, trigger_request)
# check when to create next build # check when to create next build
builds_attrs = builds_attrs.select do |build_attrs| builds_attrs = builds_attrs.select do |build_attrs|
...@@ -17,7 +21,8 @@ module Ci ...@@ -17,7 +21,8 @@ module Ci
builds_attrs.map do |build_attrs| builds_attrs.map do |build_attrs|
# don't create the same build twice # don't create the same build twice
unless commit.builds.find_by(ref: ref, tag: tag, trigger_request: trigger_request, name: build_attrs[:name]) unless @commit.builds.find_by(ref: @commit.ref, tag: @commit.tag,
trigger_request: trigger_request, name: build_attrs[:name])
build_attrs.slice!(:name, build_attrs.slice!(:name,
:commands, :commands,
:tag_list, :tag_list,
...@@ -26,17 +31,21 @@ module Ci ...@@ -26,17 +31,21 @@ module Ci
:stage, :stage,
:stage_idx) :stage_idx)
build_attrs.merge!(ref: ref, build_attrs.merge!(ref: @commit.ref,
tag: tag, tag: @commit.tag,
trigger_request: trigger_request, trigger_request: trigger_request,
user: user, user: user,
project: commit.project) project: @commit.project)
build = commit.builds.create!(build_attrs) @commit.builds.create!(build_attrs)
build.execute_hooks
build
end end
end end
end end
private
def config_processor
@config_processor ||= @commit.config_processor
end
end end
end end
...@@ -7,14 +7,14 @@ module Ci ...@@ -7,14 +7,14 @@ module Ci
# check if ref is tag # check if ref is tag
tag = project.repository.find_tag(ref).present? tag = project.repository.find_tag(ref).present?
ci_commit = project.ensure_ci_commit(commit.sha) ci_commit = project.ci_commits.create(sha: commit.sha, ref: ref, tag: tag)
trigger_request = trigger.trigger_requests.create!( trigger_request = trigger.trigger_requests.create!(
variables: variables, variables: variables,
commit: ci_commit, commit: ci_commit,
) )
if ci_commit.create_builds(ref, tag, nil, trigger_request) if ci_commit.create_builds(nil, trigger_request)
trigger_request trigger_request
end end
end end
......
...@@ -3,8 +3,9 @@ module Ci ...@@ -3,8 +3,9 @@ module Ci
def execute(project, opts) def execute(project, opts)
sha = opts[:sha] || ref_sha(project, opts[:ref]) sha = opts[:sha] || ref_sha(project, opts[:ref])
commit = project.ci_commits.find_by(sha: sha) ci_commits = project.ci_commits.where(sha: sha)
image_name = image_for_commit(commit) ci_commits = ci_commits.where(ref: opts[:ref]) if opts[:ref]
image_name = image_for_status(ci_commits.status)
image_path = Rails.root.join('public/ci', image_name) image_path = Rails.root.join('public/ci', image_name)
OpenStruct.new(path: image_path, name: image_name) OpenStruct.new(path: image_path, name: image_name)
...@@ -16,9 +17,9 @@ module Ci ...@@ -16,9 +17,9 @@ module Ci
project.commit(ref).try(:sha) if ref project.commit(ref).try(:sha) if ref
end end
def image_for_commit(commit) def image_for_status(status)
return 'build-unknown.svg' unless commit status ||= 'unknown'
'build-' + commit.status + ".svg" 'build-' + status + ".svg"
end end
end end
end end
module Commits
class ChangeService < ::BaseService
class ValidationError < StandardError; end
class ChangeError < StandardError; end
def execute
@source_project = params[:source_project] || @project
@target_branch = params[:target_branch]
@commit = params[:commit]
@create_merge_request = params[:create_merge_request].present?
check_push_permissions unless @create_merge_request
commit
rescue Repository::CommitError, Gitlab::Git::Repository::InvalidBlobName, GitHooksService::PreReceiveError,
ValidationError, ChangeError => ex
error(ex.message)
end
def commit
raise NotImplementedError
end
private
def check_push_permissions
allowed = ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(@target_branch)
unless allowed
raise ValidationError.new('You are not allowed to push into this branch')
end
true
end
def create_target_branch(new_branch)
# Temporary branch exists and contains the change commit
return success if repository.find_branch(new_branch)
result = CreateBranchService.new(@project, current_user)
.execute(new_branch, @target_branch, source_project: @source_project)
if result[:status] == :error
raise ChangeError, "There was an error creating the source branch: #{result[:message]}"
end
end
end
end
module Commits
class CherryPickService < ChangeService
def commit
cherry_pick_into = @create_merge_request ? @commit.cherry_pick_branch_name : @target_branch
cherry_pick_tree_id = repository.check_cherry_pick_content(@commit, @target_branch)
if cherry_pick_tree_id
create_target_branch(cherry_pick_into) if @create_merge_request
repository.cherry_pick(current_user, @commit, cherry_pick_into, cherry_pick_tree_id)
success
else
error_msg = "Sorry, we cannot cherry-pick this #{@commit.change_type_title} automatically.
It may have already been cherry-picked, or a more recent commit may have updated some of its content."
raise ChangeError, error_msg
end
end
end
end
module Commits module Commits
class RevertService < ::BaseService class RevertService < ChangeService
class ValidationError < StandardError; end
class ReversionError < StandardError; end
def execute
@source_project = params[:source_project] || @project
@target_branch = params[:target_branch]
@commit = params[:commit]
@create_merge_request = params[:create_merge_request].present?
check_push_permissions unless @create_merge_request
commit
rescue Repository::CommitError, Gitlab::Git::Repository::InvalidBlobName, GitHooksService::PreReceiveError,
ValidationError, ReversionError => ex
error(ex.message)
end
def commit def commit
revert_into = @create_merge_request ? @commit.revert_branch_name : @target_branch revert_into = @create_merge_request ? @commit.revert_branch_name : @target_branch
revert_tree_id = repository.check_revert_content(@commit, @target_branch) revert_tree_id = repository.check_revert_content(@commit, @target_branch)
...@@ -26,34 +10,10 @@ module Commits ...@@ -26,34 +10,10 @@ module Commits
repository.revert(current_user, @commit, revert_into, revert_tree_id) repository.revert(current_user, @commit, revert_into, revert_tree_id)
success success
else else
error_msg = "Sorry, we cannot revert this #{params[:revert_type_title]} automatically. error_msg = "Sorry, we cannot revert this #{@commit.change_type_title} automatically.
It may have already been reverted, or a more recent commit may have updated some of its content." It may have already been reverted, or a more recent commit may have updated some of its content."
raise ReversionError, error_msg raise ChangeError, error_msg
end end
end end
private
def create_target_branch(new_branch)
# Temporary branch exists and contains the revert commit
return success if repository.find_branch(new_branch)
result = CreateBranchService.new(@project, current_user)
.execute(new_branch, @target_branch, source_project: @source_project)
if result[:status] == :error
raise ReversionError, "There was an error creating the source branch: #{result[:message]}"
end
end
def check_push_permissions
allowed = ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(@target_branch)
unless allowed
raise ValidationError.new('You are not allowed to push into this branch')
end
true
end
end end
end end
...@@ -2,6 +2,7 @@ class CreateCommitBuildsService ...@@ -2,6 +2,7 @@ class CreateCommitBuildsService
def execute(project, user, params) def execute(project, user, params)
return false unless project.builds_enabled? return false unless project.builds_enabled?
before_sha = params[:checkout_sha] || params[:before]
sha = params[:checkout_sha] || params[:after] sha = params[:checkout_sha] || params[:after]
origin_ref = params[:ref] origin_ref = params[:ref]
...@@ -10,15 +11,16 @@ class CreateCommitBuildsService ...@@ -10,15 +11,16 @@ class CreateCommitBuildsService
end end
ref = Gitlab::Git.ref_name(origin_ref) ref = Gitlab::Git.ref_name(origin_ref)
tag = Gitlab::Git.tag_ref?(origin_ref)
# Skip branch removal # Skip branch removal
if sha == Gitlab::Git::BLANK_SHA if sha == Gitlab::Git::BLANK_SHA
return false return false
end end
commit = project.ci_commit(sha) commit = project.ci_commit(sha, ref)
unless commit unless commit
commit = project.ci_commits.new(sha: sha) commit = project.ci_commits.new(sha: sha, ref: ref, before_sha: before_sha, tag: tag)
# Skip creating ci_commit when no gitlab-ci.yml is found # Skip creating ci_commit when no gitlab-ci.yml is found
unless commit.ci_yaml_file unless commit.ci_yaml_file
...@@ -32,10 +34,10 @@ class CreateCommitBuildsService ...@@ -32,10 +34,10 @@ class CreateCommitBuildsService
# Skip creating builds for commits that have [ci skip] # Skip creating builds for commits that have [ci skip]
unless commit.skip_ci? unless commit.skip_ci?
# Create builds for commit # Create builds for commit
tag = Gitlab::Git.tag_ref?(origin_ref) commit.create_builds(user)
commit.create_builds(ref, tag, user)
end end
commit.touch
commit commit
end end
end end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment