Commit 16bd4e56 authored by Douwe Maan's avatar Douwe Maan

Merge branch 'master' into workhorse-helpers

parents a9857f8c e0f3e44b
...@@ -61,6 +61,8 @@ update-knapsack: ...@@ -61,6 +61,8 @@ update-knapsack:
- scripts/merge-reports knapsack/rspec_report.json knapsack/rspec_node_*.json - scripts/merge-reports knapsack/rspec_report.json knapsack/rspec_node_*.json
- scripts/merge-reports knapsack/spinach_report.json knapsack/spinach_node_*.json - scripts/merge-reports knapsack/spinach_report.json knapsack/spinach_node_*.json
- rm -f knapsack/*_node_*.json - rm -f knapsack/*_node_*.json
only:
- master
# Execute all testing suites # Execute all testing suites
......
...@@ -1088,6 +1088,9 @@ Rails/TimeZone: ...@@ -1088,6 +1088,9 @@ Rails/TimeZone:
Rails/Validation: Rails/Validation:
Enabled: false Enabled: false
Rails/UniqBeforePluck:
Enabled: false
##################### RSpec ################################## ##################### RSpec ##################################
# Check that instances are not being stubbed globally. # Check that instances are not being stubbed globally.
......
...@@ -3,13 +3,16 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -3,13 +3,16 @@ Please view this file on the master branch, on stable branches it's out of date.
v 8.9.0 (unreleased) v 8.9.0 (unreleased)
- Bulk assign/unassign labels to issues. - Bulk assign/unassign labels to issues.
- Ability to prioritize labels !4009 / !3205 (Thijs Wouters) - Ability to prioritize labels !4009 / !3205 (Thijs Wouters)
- Fix endless redirections when accessing user OAuth applications when they are disabled
- Allow enabling wiki page events from Webhook management UI - Allow enabling wiki page events from Webhook management UI
- Bump rouge to 1.11.0 - Bump rouge to 1.11.0
- Make EmailsOnPushWorker use Sidekiq mailers queue - Make EmailsOnPushWorker use Sidekiq mailers queue
- Fix wiki page events' webhook to point to the wiki repository - Fix wiki page events' webhook to point to the wiki repository
- Fix issue todo not remove when leave project !4150 (Long Nguyen) - Fix issue todo not remove when leave project !4150 (Long Nguyen)
- Allow customisable text on the 'nearly there' page after a user signs up
- Bump recaptcha gem to 3.0.0 to remove deprecated stoken support - Bump recaptcha gem to 3.0.0 to remove deprecated stoken support
- Allow forking projects with restricted visibility level - Allow forking projects with restricted visibility level
- Added descriptions to notification settings dropdown
- Improve note validation to prevent errors when creating invalid note via API - Improve note validation to prevent errors when creating invalid note via API
- Reduce number of fog gem dependencies - Reduce number of fog gem dependencies
- Remove project notification settings associated with deleted projects - Remove project notification settings associated with deleted projects
...@@ -17,14 +20,17 @@ v 8.9.0 (unreleased) ...@@ -17,14 +20,17 @@ v 8.9.0 (unreleased)
- Redesign navigation for project pages - Redesign navigation for project pages
- Fix groups API to list only user's accessible projects - Fix groups API to list only user's accessible projects
- Redesign account and email confirmation emails - Redesign account and email confirmation emails
- `git clone https://host/namespace/project` now works, in addition to using the `.git` suffix
- Bump nokogiri to 1.6.8 - Bump nokogiri to 1.6.8
- Use gitlab-shell v3.0.0 - Use gitlab-shell v3.0.0
- Upgrade to jQuery 2
- Use Knapsack to evenly distribute tests across multiple nodes - Use Knapsack to evenly distribute tests across multiple nodes
- Add `sha` parameter to MR merge API, to ensure only reviewed changes are merged - Add `sha` parameter to MR merge API, to ensure only reviewed changes are merged
- Don't allow MRs to be merged when commits were added since the last review / page load - Don't allow MRs to be merged when commits were added since the last review / page load
- Add DB index on users.state - Add DB index on users.state
- Add rake task 'gitlab:db:configure' for conditionally seeding or migrating the database - Add rake task 'gitlab:db:configure' for conditionally seeding or migrating the database
- Changed the Slack build message to use the singular duration if necessary (Aran Koning) - Changed the Slack build message to use the singular duration if necessary (Aran Koning)
- Links from a wiki page to other wiki pages should be rewritten as expected
- Fix issues filter when ordering by milestone - Fix issues filter when ordering by milestone
- Todos will display target state if issuable target is 'Closed' or 'Merged' - Todos will display target state if issuable target is 'Closed' or 'Merged'
- Fix bug when sorting issues by milestone due date and filtering by two or more labels - Fix bug when sorting issues by milestone due date and filtering by two or more labels
...@@ -35,11 +41,14 @@ v 8.9.0 (unreleased) ...@@ -35,11 +41,14 @@ v 8.9.0 (unreleased)
- Use downcased path to container repository as this is expected path by Docker - Use downcased path to container repository as this is expected path by Docker
- Projects pending deletion will render a 404 page - Projects pending deletion will render a 404 page
- Measure queue duration between gitlab-workhorse and Rails - Measure queue duration between gitlab-workhorse and Rails
- Make Omniauth providers specs to not modify global configuration
- Make authentication service for Container Registry to be compatible with < Docker 1.11 - Make authentication service for Container Registry to be compatible with < Docker 1.11
- Add Application Setting to configure Container Registry token expire delay (default 5min) - Add Application Setting to configure Container Registry token expire delay (default 5min)
- Cache assigned issue and merge request counts in sidebar nav - Cache assigned issue and merge request counts in sidebar nav
- Use Knapsack only in CI environment - Use Knapsack only in CI environment
- Cache project build count in sidebar nav - Cache project build count in sidebar nav
- Add milestone expire date to the right sidebar
- Fix markdown_spec to use before instead of before(:all) to properly cleanup database after testing
- Reduce number of queries needed to render issue labels in the sidebar - Reduce number of queries needed to render issue labels in the sidebar
- Improve error handling importing projects - Improve error handling importing projects
- Remove duplicated notification settings - Remove duplicated notification settings
...@@ -48,18 +57,23 @@ v 8.9.0 (unreleased) ...@@ -48,18 +57,23 @@ v 8.9.0 (unreleased)
- Add workhorse controller and API helpers - Add workhorse controller and API helpers
- An indicator is now displayed at the top of the comment field for confidential issues. - An indicator is now displayed at the top of the comment field for confidential issues.
- RepositoryCheck::SingleRepositoryWorker public and private methods are now instrumented - RepositoryCheck::SingleRepositoryWorker public and private methods are now instrumented
- Improve issuables APIs performance when accessing notes !4471
- External links now open in a new tab
- Markdown editor now correctly resets the input value on edit cancellation !4175
- Toggling a task list item in a issue/mr description does not creates a Todo for mentions
- Improved UX of date pickers on issue & milestone forms
v 8.8.4 (unreleased) v 8.8.5 (unreleased)
- Ensure branch cleanup regardless of whether the GitHub import process succeeds - Ensure branch cleanup regardless of whether the GitHub import process succeeds
- Fix issue with arrow keys not working in search autocomplete dropdown - Fix issue with arrow keys not working in search autocomplete dropdown
- Fix todos page throwing errors when you have a project pending deletion - Fix todos page throwing errors when you have a project pending deletion
- Reduce number of SQL queries when rendering user references - Reduce number of SQL queries when rendering user references
- Upgrade to jQuery 2
- Remove prev/next buttons on issues and merge requests
- Import GitHub repositories respecting the API rate limit - Import GitHub repositories respecting the API rate limit
- Fix importer for GitHub comments on diff - Fix importer for GitHub comments on diff
- Disable Webhooks before proceeding with the GitHub import - Disable Webhooks before proceeding with the GitHub import
- Added descriptions to notification settings dropdown
v 8.8.4
- Fix LDAP-based login for users with 2FA enabled. !4493
v 8.8.3 v 8.8.3
- Fix 404 page when viewing TODOs that contain milestones or labels in different projects. !4312 - Fix 404 page when viewing TODOs that contain milestones or labels in different projects. !4312
...@@ -171,6 +185,7 @@ v 8.8.0 ...@@ -171,6 +185,7 @@ v 8.8.0
- Fixed advice on invalid permissions on upload path !2948 (Ludovic Perrine) - Fixed advice on invalid permissions on upload path !2948 (Ludovic Perrine)
- Allows MR authors to have the source branch removed when merging the MR. !2801 (Jeroen Jacobs) - Allows MR authors to have the source branch removed when merging the MR. !2801 (Jeroen Jacobs)
- When creating a .gitignore file a dropdown with templates will be provided - When creating a .gitignore file a dropdown with templates will be provided
- Shows the issue/MR list search/filter form and corrects the mobile styling for guest users. #17562
v 8.7.7 v 8.7.7
- Fix import by `Any Git URL` broken if the URL contains a space - Fix import by `Any Git URL` broken if the URL contains a space
......
...@@ -86,6 +86,7 @@ gem 'dropzonejs-rails', '~> 0.7.1' ...@@ -86,6 +86,7 @@ gem 'dropzonejs-rails', '~> 0.7.1'
# for backups # for backups
gem 'fog-aws', '~> 0.9' gem 'fog-aws', '~> 0.9'
gem 'fog-azure', '~> 0.0'
gem 'fog-core', '~> 1.40' gem 'fog-core', '~> 1.40'
gem 'fog-local', '~> 0.3' gem 'fog-local', '~> 0.3'
gem 'fog-google', '~> 0.3' gem 'fog-google', '~> 0.3'
......
...@@ -70,6 +70,21 @@ GEM ...@@ -70,6 +70,21 @@ GEM
descendants_tracker (~> 0.0.4) descendants_tracker (~> 0.0.4)
ice_nine (~> 0.11.0) ice_nine (~> 0.11.0)
thread_safe (~> 0.3, >= 0.3.1) thread_safe (~> 0.3, >= 0.3.1)
azure (0.7.5)
addressable (~> 2.3)
azure-core (~> 0.1)
faraday (~> 0.9)
faraday_middleware (~> 0.10)
json (~> 1.8)
mime-types (>= 1, < 3.0)
nokogiri (~> 1.6)
systemu (~> 2.6)
thor (~> 0.19)
uuid (~> 2.0)
azure-core (0.1.2)
faraday (~> 0.9)
faraday_middleware (~> 0.10)
nokogiri (~> 1.6)
babosa (1.0.2) babosa (1.0.2)
base32 (0.3.2) base32 (0.3.2)
bcrypt (3.1.11) bcrypt (3.1.11)
...@@ -213,6 +228,11 @@ GEM ...@@ -213,6 +228,11 @@ GEM
fog-json (~> 1.0) fog-json (~> 1.0)
fog-xml (~> 0.1) fog-xml (~> 0.1)
ipaddress (~> 0.8) ipaddress (~> 0.8)
fog-azure (0.0.2)
azure (~> 0.6)
fog-core (~> 1.27)
fog-json (~> 1.0)
fog-xml (~> 0.1)
fog-core (1.40.0) fog-core (1.40.0)
builder builder
excon (~> 0.49) excon (~> 0.49)
...@@ -856,6 +876,7 @@ DEPENDENCIES ...@@ -856,6 +876,7 @@ DEPENDENCIES
flay flay
flog flog
fog-aws (~> 0.9) fog-aws (~> 0.9)
fog-azure (~> 0.0)
fog-core (~> 1.40) fog-core (~> 1.40)
fog-google (~> 0.3) fog-google (~> 0.3)
fog-local (~> 0.3) fog-local (~> 0.3)
......
class @Activities class @Activities
constructor: -> constructor: ->
Pager.init 20, true Pager.init 20, true, false, @updateTooltips
$(".event-filter-link").on "click", (event) => $(".event-filter-link").on "click", (event) =>
event.preventDefault() event.preventDefault()
@toggleFilter($(event.currentTarget)) @toggleFilter($(event.currentTarget))
@reloadActivities() @reloadActivities()
updateTooltips: ->
gl.utils.localTimeAgo($('.js-timeago', '#activity'))
reloadActivities: -> reloadActivities: ->
$(".content_list").html '' $(".content_list").html ''
Pager.init 20, true Pager.init 20, true
......
...@@ -35,7 +35,6 @@ ...@@ -35,7 +35,6 @@
#= require raphael #= require raphael
#= require g.raphael #= require g.raphael
#= require g.bar #= require g.bar
#= require Chart
#= require branch-graph #= require branch-graph
#= require ace/ace #= require ace/ace
#= require ace/ext-searchbox #= require ace/ext-searchbox
...@@ -226,6 +225,10 @@ $ -> ...@@ -226,6 +225,10 @@ $ ->
form = btn.closest("form") form = btn.closest("form")
new ConfirmDangerModal(form, text) new ConfirmDangerModal(form, text)
$(document).on 'click', 'button', ->
$(this).blur()
$('input[type="search"]').each -> $('input[type="search"]').each ->
$this = $(this) $this = $(this)
$this.attr 'value', $this.val() $this.attr 'value', $this.val()
...@@ -268,5 +271,6 @@ $ -> ...@@ -268,5 +271,6 @@ $ ->
.on "resize", (e) -> .on "resize", (e) ->
fitSidebarForSize() fitSidebarForSize()
gl.awardsHandler = new AwardsHandler()
checkInitialSidebarSize() checkInitialSidebarSize()
new Aside() new Aside()
...@@ -65,7 +65,7 @@ class @AwardsHandler ...@@ -65,7 +65,7 @@ class @AwardsHandler
$addBtn.removeClass 'is-loading' $addBtn.removeClass 'is-loading'
$menu = $('.emoji-menu') $menu = $('.emoji-menu')
@positionMenu($menu, $addBtn) @positionMenu($menu, $addBtn)
@renderFrequentlyUsedBlock() @renderFrequentlyUsedBlock() unless @frequentEmojiBlockRendered
setTimeout => setTimeout =>
$menu.addClass 'is-visible' $menu.addClass 'is-visible'
...@@ -100,7 +100,7 @@ class @AwardsHandler ...@@ -100,7 +100,7 @@ class @AwardsHandler
$menu.css(css) $menu.css(css)
addAward: (votesBlock, awardUrl, emoji, checkMutuality = yes, callback) -> addAward: (votesBlock, awardUrl, emoji, checkMutuality = true, callback) ->
emoji = @normilizeEmojiName emoji emoji = @normilizeEmojiName emoji
...@@ -111,7 +111,7 @@ class @AwardsHandler ...@@ -111,7 +111,7 @@ class @AwardsHandler
$('.emoji-menu').removeClass 'is-visible' $('.emoji-menu').removeClass 'is-visible'
addAwardToEmojiBar: (votesBlock, emoji, checkForMutuality = yes) -> addAwardToEmojiBar: (votesBlock, emoji, checkForMutuality = true) ->
@checkMutuality votesBlock, emoji if checkForMutuality @checkMutuality votesBlock, emoji if checkForMutuality
@addEmojiToFrequentlyUsedList emoji @addEmojiToFrequentlyUsedList emoji
...@@ -153,7 +153,7 @@ class @AwardsHandler ...@@ -153,7 +153,7 @@ class @AwardsHandler
if isAlreadyVoted if isAlreadyVoted
@showEmojiLoader $emojiButton @showEmojiLoader $emojiButton
@addAward votesBlock, awardUrl, mutualVote, no, -> @addAward votesBlock, awardUrl, mutualVote, false, ->
$emojiButton.removeClass 'is-loading' $emojiButton.removeClass 'is-loading'
...@@ -282,7 +282,7 @@ class @AwardsHandler ...@@ -282,7 +282,7 @@ class @AwardsHandler
@createEmojiMenu @getAwardMenuUrl(), => @createEmoji_ votesBlock, emoji @createEmojiMenu @getAwardMenuUrl(), => @createEmoji_ votesBlock, emoji
getAwardMenuUrl: -> return gl.awardMenuUrl getAwardMenuUrl: -> return gon.award_menu_url
resolveNameToCssClass: (emoji) -> resolveNameToCssClass: (emoji) ->
...@@ -336,13 +336,15 @@ class @AwardsHandler ...@@ -336,13 +336,15 @@ class @AwardsHandler
if $.cookie 'frequently_used_emojis' if $.cookie 'frequently_used_emojis'
frequentlyUsedEmojis = @getFrequentlyUsedEmojis() frequentlyUsedEmojis = @getFrequentlyUsedEmojis()
ul = $("<ul class='clearfix emoji-menu-list'>") ul = $("<ul class='clearfix emoji-menu-list frequent-emojis'>")
for emoji in frequentlyUsedEmojis for emoji in frequentlyUsedEmojis
$(".emoji-menu-content [data-emoji='#{emoji}']").closest('li').clone().appendTo(ul) $(".emoji-menu-content [data-emoji='#{emoji}']").closest('li').clone().appendTo(ul)
$('input.emoji-search').after(ul).after($('<h5>').text('Frequently used')) $('input.emoji-search').after(ul).after($('<h5>').text('Frequently used'))
@frequentEmojiBlockRendered = true
setupSearch: -> setupSearch: ->
...@@ -365,4 +367,4 @@ class @AwardsHandler ...@@ -365,4 +367,4 @@ class @AwardsHandler
searchEmojis: (term) -> searchEmojis: (term) ->
$(".emoji-menu-content [data-emoji*='#{term}']").closest('li').clone() $(".emoji-menu-list:not(.frequent-emojis) [data-emoji*='#{term}']").closest('li').clone()
...@@ -23,7 +23,6 @@ class Dispatcher ...@@ -23,7 +23,6 @@ class Dispatcher
new Issue() new Issue()
shortcut_handler = new ShortcutsIssuable() shortcut_handler = new ShortcutsIssuable()
new ZenMode() new ZenMode()
gl.awardsHandler = new AwardsHandler()
when 'projects:milestones:show', 'groups:milestones:show', 'dashboard:milestones:show' when 'projects:milestones:show', 'groups:milestones:show', 'dashboard:milestones:show'
new Milestone() new Milestone()
when 'dashboard:todos:index' when 'dashboard:todos:index'
...@@ -54,7 +53,6 @@ class Dispatcher ...@@ -54,7 +53,6 @@ class Dispatcher
new Diff() new Diff()
shortcut_handler = new ShortcutsIssuable(true) shortcut_handler = new ShortcutsIssuable(true)
new ZenMode() new ZenMode()
gl.awardsHandler = new AwardsHandler()
when "projects:merge_requests:diffs" when "projects:merge_requests:diffs"
new Diff() new Diff()
new ZenMode() new ZenMode()
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
window.GitLab ?= {} window.GitLab ?= {}
GitLab.GfmAutoComplete = GitLab.GfmAutoComplete =
dataLoading: false dataLoading: false
dataLoaded: false
dataSource: '' dataSource: ''
...@@ -22,6 +23,24 @@ GitLab.GfmAutoComplete = ...@@ -22,6 +23,24 @@ GitLab.GfmAutoComplete =
Milestones: Milestones:
template: '<li>${title}</li>' template: '<li>${title}</li>'
Loading:
template: '<li><i class="fa fa-refresh fa-spin"></i> Loading...</li>'
DefaultOptions:
sorter: (query, items, searchKey) ->
return items if items[0].name? and items[0].name is 'loading'
$.fn.atwho.default.callbacks.sorter(query, items, searchKey)
filter: (query, data, searchKey) ->
return data if data[0] is 'loading'
$.fn.atwho.default.callbacks.filter(query, data, searchKey)
beforeInsert: (value) ->
if not GitLab.GfmAutoComplete.dataLoaded
@at
else
value
# Add GFM auto-completion to all input fields, that accept GFM input. # Add GFM auto-completion to all input fields, that accept GFM input.
setup: (wrap) -> setup: (wrap) ->
@input = $('.js-gfm-input') @input = $('.js-gfm-input')
...@@ -53,18 +72,37 @@ GitLab.GfmAutoComplete = ...@@ -53,18 +72,37 @@ GitLab.GfmAutoComplete =
# Emoji # Emoji
@input.atwho @input.atwho
at: ':' at: ':'
displayTpl: @Emoji.template displayTpl: (value) =>
if value.path?
@Emoji.template
else
@Loading.template
insertTpl: ':${name}:' insertTpl: ':${name}:'
data: ['loading']
callbacks:
sorter: @DefaultOptions.sorter
filter: @DefaultOptions.filter
beforeInsert: @DefaultOptions.beforeInsert
# Team Members # Team Members
@input.atwho @input.atwho
at: '@' at: '@'
displayTpl: @Members.template displayTpl: (value) =>
if value.username?
@Members.template
else
@Loading.template
insertTpl: '${atwho-at}${username}' insertTpl: '${atwho-at}${username}'
searchKey: 'search' searchKey: 'search'
data: ['loading']
callbacks: callbacks:
sorter: @DefaultOptions.sorter
filter: @DefaultOptions.filter
beforeInsert: @DefaultOptions.beforeInsert
beforeSave: (members) -> beforeSave: (members) ->
$.map members, (m) -> $.map members, (m) ->
return m if not m.username?
title = m.name title = m.name
title += " (#{m.count})" if m.count title += " (#{m.count})" if m.count
...@@ -76,11 +114,21 @@ GitLab.GfmAutoComplete = ...@@ -76,11 +114,21 @@ GitLab.GfmAutoComplete =
at: '#' at: '#'
alias: 'issues' alias: 'issues'
searchKey: 'search' searchKey: 'search'
displayTpl: @Issues.template displayTpl: (value) =>
if value.title?
@Issues.template
else
@Loading.template
data: ['loading']
insertTpl: '${atwho-at}${id}' insertTpl: '${atwho-at}${id}'
callbacks: callbacks:
sorter: @DefaultOptions.sorter
filter: @DefaultOptions.filter
beforeInsert: @DefaultOptions.beforeInsert
beforeSave: (issues) -> beforeSave: (issues) ->
$.map issues, (i) -> $.map issues, (i) ->
return i if not i.title?
id: i.iid id: i.iid
title: sanitize(i.title) title: sanitize(i.title)
search: "#{i.iid} #{i.title}" search: "#{i.iid} #{i.title}"
...@@ -89,11 +137,18 @@ GitLab.GfmAutoComplete = ...@@ -89,11 +137,18 @@ GitLab.GfmAutoComplete =
at: '%' at: '%'
alias: 'milestones' alias: 'milestones'
searchKey: 'search' searchKey: 'search'
displayTpl: @Milestones.template displayTpl: (value) =>
if value.title?
@Milestones.template
else
@Loading.template
insertTpl: '${atwho-at}"${title}"' insertTpl: '${atwho-at}"${title}"'
data: ['loading']
callbacks: callbacks:
beforeSave: (milestones) -> beforeSave: (milestones) ->
$.map milestones, (m) -> $.map milestones, (m) ->
return m if not m.title?
id: m.iid id: m.iid
title: sanitize(m.title) title: sanitize(m.title)
search: "#{m.title}" search: "#{m.title}"
...@@ -102,11 +157,21 @@ GitLab.GfmAutoComplete = ...@@ -102,11 +157,21 @@ GitLab.GfmAutoComplete =
at: '!' at: '!'
alias: 'mergerequests' alias: 'mergerequests'
searchKey: 'search' searchKey: 'search'
displayTpl: @Issues.template displayTpl: (value) =>
if value.title?
@Issues.template
else
@Loading.template
data: ['loading']
insertTpl: '${atwho-at}${id}' insertTpl: '${atwho-at}${id}'
callbacks: callbacks:
sorter: @DefaultOptions.sorter
filter: @DefaultOptions.filter
beforeInsert: @DefaultOptions.beforeInsert
beforeSave: (merges) -> beforeSave: (merges) ->
$.map merges, (m) -> $.map merges, (m) ->
return m if not m.title?
id: m.iid id: m.iid
title: sanitize(m.title) title: sanitize(m.title)
search: "#{m.iid} #{m.title}" search: "#{m.iid} #{m.title}"
...@@ -118,6 +183,8 @@ GitLab.GfmAutoComplete = ...@@ -118,6 +183,8 @@ GitLab.GfmAutoComplete =
$.getJSON(dataSource) $.getJSON(dataSource)
loadData: (data) -> loadData: (data) ->
@dataLoaded = true
# load members # load members
@input.atwho 'load', '@', data.members @input.atwho 'load', '@', data.members
# load issues # load issues
...@@ -128,3 +195,7 @@ GitLab.GfmAutoComplete = ...@@ -128,3 +195,7 @@ GitLab.GfmAutoComplete =
@input.atwho 'load', 'mergerequests', data.mergerequests @input.atwho 'load', 'mergerequests', data.mergerequests
# load emojis # load emojis
@input.atwho 'load', ':', data.emojis @input.atwho 'load', ':', data.emojis
# This trigger at.js again
# otherwise we would be stuck with loading until the user types
$(':focus').trigger('keyup')
...@@ -211,6 +211,7 @@ class GitLabDropdown ...@@ -211,6 +211,7 @@ class GitLabDropdown
@dropdown.on "shown.bs.dropdown", @opened @dropdown.on "shown.bs.dropdown", @opened
@dropdown.on "hidden.bs.dropdown", @hidden @dropdown.on "hidden.bs.dropdown", @hidden
$(@el).on "update.label", @updateLabel
@dropdown.on "click", ".dropdown-menu, .dropdown-menu-close", @shouldPropagate @dropdown.on "click", ".dropdown-menu, .dropdown-menu-close", @shouldPropagate
@dropdown.on 'keyup', (e) => @dropdown.on 'keyup', (e) =>
if e.which is 27 # Escape key if e.which is 27 # Escape key
...@@ -453,7 +454,7 @@ class GitLabDropdown ...@@ -453,7 +454,7 @@ class GitLabDropdown
# Toggle the dropdown label # Toggle the dropdown label
if @options.toggleLabel if @options.toggleLabel
$(@el).find(".dropdown-toggle-text").text @options.toggleLabel @updateLabel()
else else
selectedObject selectedObject
else if el.hasClass(INDETERMINATE_CLASS) else if el.hasClass(INDETERMINATE_CLASS)
...@@ -480,7 +481,7 @@ class GitLabDropdown ...@@ -480,7 +481,7 @@ class GitLabDropdown
# Toggle the dropdown label # Toggle the dropdown label
if @options.toggleLabel if @options.toggleLabel
$(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject, el) @updateLabel(selectedObject, el)
if value? if value?
if !field.length and fieldName if !field.length and fieldName
@addInput(fieldName, value) @addInput(fieldName, value)
...@@ -579,6 +580,9 @@ class GitLabDropdown ...@@ -579,6 +580,9 @@ class GitLabDropdown
# Scroll the dropdown content up # Scroll the dropdown content up
$dropdownContent.scrollTop(listItemTop - dropdownContentTop) $dropdownContent.scrollTop(listItemTop - dropdownContentTop)
updateLabel: (selected = null, el = null) =>
$(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selected, el)
$.fn.glDropdown = (opts) -> $.fn.glDropdown = (opts) ->
return @.each -> return @.each ->
if (!$.data @, 'glDropdown') if (!$.data @, 'glDropdown')
......
...@@ -4,4 +4,5 @@ ...@@ -4,4 +4,5 @@
# It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the # It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
# the compiled file. # the compiled file.
# #
#= require Chart
#= require_tree . #= require_tree .
...@@ -6,12 +6,18 @@ issuable_created = false ...@@ -6,12 +6,18 @@ issuable_created = false
Issuable.initTemplates() Issuable.initTemplates()
Issuable.initSearch() Issuable.initSearch()
Issuable.initChecks() Issuable.initChecks()
Issuable.initLabelFilterRemove()
initTemplates: -> initTemplates: ->
Issuable.labelRow = _.template( Issuable.labelRow = _.template(
'<% _.each(labels, function(label){ %> '<% _.each(labels, function(label){ %>
<span class="label-row"> <span class="label-row btn-group" role="group" aria-label="<%= _.escape(label.title) %>" style="color: <%= label.text_color %>;">
<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> <a href="#" class="btn btn-transparent has-tooltip" style="background-color: <%= label.color %>;" title="<%= _.escape(label.description) %>" data-container="body">
<%= _.escape(label.title) %>
</a>
<button type="button" class="btn btn-transparent label-remove js-label-filter-remove" style="background-color: <%= label.color %>;" data-label="<%= _.escape(label.title) %>">
<i class="fa fa-times"></i>
</button>
</span> </span>
<% }); %>' <% }); %>'
) )
...@@ -35,6 +41,21 @@ issuable_created = false ...@@ -35,6 +41,21 @@ issuable_created = false
Issuable.filterResults $form Issuable.filterResults $form
, 500) , 500)
initLabelFilterRemove: ->
$(document)
.off 'click', '.js-label-filter-remove'
.on 'click', '.js-label-filter-remove', (e) ->
$button = $(@)
# Remove the label input box
$('input[name="label_name[]"]')
.filter -> @value is $button.data('label')
.remove()
# Submit the form to get new data
Issuable.filterResults $('.filter-form')
$('.js-label-select').trigger('update.label')
toggleLabelFilters: -> toggleLabelFilters: ->
$filteredLabels = $('.filtered-labels') $filteredLabels = $('.filtered-labels')
if $filteredLabels.find('.label-row').length > 0 if $filteredLabels.find('.label-row').length > 0
......
((w) ->
jQuery.timefor = (time, suffix, expiredLabel) ->
return '' unless time
suffix or= 'remaining'
expiredLabel or= 'Past due'
jQuery.timeago.settings.allowFuture = yes
{ suffixFromNow } = jQuery.timeago.settings.strings
jQuery.timeago.settings.strings.suffixFromNow = suffix
timefor = $.timeago time
if timefor.indexOf('ago') > -1
timefor = expiredLabel
jQuery.timeago.settings.strings.suffixFromNow = suffixFromNow
return timefor
) window
...@@ -12,6 +12,13 @@ ...@@ -12,6 +12,13 @@
$el.attr('title', gl.utils.formatDate($el.attr('datetime'))) $el.attr('title', gl.utils.formatDate($el.attr('datetime')))
) )
$timeagoEls.timeago() if setTimeago if setTimeago
$timeagoEls.timeago()
$timeagoEls.tooltip('destroy')
# Recreate with custom template
$timeagoEls.tooltip(
template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
)
) window ) window
...@@ -24,11 +24,21 @@ class @MilestoneSelect ...@@ -24,11 +24,21 @@ class @MilestoneSelect
if issueUpdateURL if issueUpdateURL
milestoneLinkTemplate = _.template( milestoneLinkTemplate = _.template(
'<a href="/<%= namespace %>/<%= path %>/milestones/<%= iid %>"><%= _.escape(title) %></a>' '<a href="/<%= namespace %>/<%= path %>/milestones/<%= iid %>">
<span class="has-tooltip" data-container="body" title="<%= remaining %>">
<%= _.escape(title) %>
</span>
</a>'
) )
milestoneLinkNoneTemplate = '<div class="light">None</div>' milestoneLinkNoneTemplate = '<div class="light">None</div>'
collapsedSidebarLabelTemplate = _.template(
'<span class="has-tooltip" data-container="body" title="<%= remaining %>" data-placement="left">
<%= _.escape(title) %>
</span>'
)
$dropdown.glDropdown( $dropdown.glDropdown(
data: (term, callback) -> data: (term, callback) ->
$.ajax( $.ajax(
...@@ -122,8 +132,9 @@ class @MilestoneSelect ...@@ -122,8 +132,9 @@ class @MilestoneSelect
if data.milestone? if data.milestone?
data.milestone.namespace = _this.currentProject.namespace data.milestone.namespace = _this.currentProject.namespace
data.milestone.path = _this.currentProject.path data.milestone.path = _this.currentProject.path
data.milestone.remaining = $.timefor data.milestone.due_date
$value.html(milestoneLinkTemplate(data.milestone)) $value.html(milestoneLinkTemplate(data.milestone))
$sidebarCollapsedValue.find('span').text(data.milestone.title) $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone))
else else
$value.html(milestoneLinkNoneTemplate) $value.html(milestoneLinkNoneTemplate)
$sidebarCollapsedValue.find('span').text('No') $sidebarCollapsedValue.find('span').text('No')
......
...@@ -354,8 +354,7 @@ class @Notes ...@@ -354,8 +354,7 @@ class @Notes
Called in response to clicking the edit note link Called in response to clicking the edit note link
Replaces the note text with the note edit form Replaces the note text with the note edit form
Adds a hidden div with the original content of the note to fill the edit note form with Adds a data attribute to the form with the original content of the note for cancellations
if the user cancels
### ###
showEditForm: (e, scrollTo, myLastNote) -> showEditForm: (e, scrollTo, myLastNote) ->
e.preventDefault() e.preventDefault()
...@@ -371,6 +370,8 @@ class @Notes ...@@ -371,6 +370,8 @@ class @Notes
done = ($noteText) -> done = ($noteText) ->
# Neat little trick to put the cursor at the end # Neat little trick to put the cursor at the end
noteTextVal = $noteText.val() noteTextVal = $noteText.val()
# Store the original note text in a data attribute to retrieve if a user cancels edit.
form.find('form.edit-note').data 'original-note', noteTextVal
$noteText.val('').val(noteTextVal); $noteText.val('').val(noteTextVal);
new GLForm form new GLForm form
...@@ -393,14 +394,16 @@ class @Notes ...@@ -393,14 +394,16 @@ class @Notes
### ###
Called in response to clicking the edit note link Called in response to clicking the edit note link
Hides edit form Hides edit form and restores the original note text to the editor textarea.
### ###
cancelEdit: (e) -> cancelEdit: (e) ->
e.preventDefault() e.preventDefault()
note = $(this).closest(".note") note = $(this).closest(".note")
form = note.find(".current-note-edit-form")
note.removeClass "is-editting" note.removeClass "is-editting"
note.find(".current-note-edit-form") form.removeClass("current-note-edit-form")
.removeClass("current-note-edit-form") # Replace markdown textarea text with original note text.
form.find(".js-note-text").val(form.find('form.edit-note').data('original-note'))
### ###
Called in response to deleting a note of any kind. Called in response to deleting a note of any kind.
......
@Pager = @Pager =
init: (@limit = 0, preload, @disable = false) -> init: (@limit = 0, preload, @disable = false, @callback = $.noop) ->
@loading = $('.loading').first() @loading = $('.loading').first()
if preload if preload
...@@ -19,6 +19,7 @@ ...@@ -19,6 +19,7 @@
@loading.hide() @loading.hide()
success: (data) -> success: (data) ->
Pager.append(data.count, data.html) Pager.append(data.count, data.html)
Pager.callback()
dataType: "json" dataType: "json"
append: (count, html) -> append: (count, html) ->
......
...@@ -19,3 +19,8 @@ class @Subscription ...@@ -19,3 +19,8 @@ class @Subscription
action = if status == 'subscribed' then 'Unsubscribe' else 'Subscribe' action = if status == 'subscribed' then 'Unsubscribe' else 'Subscribe'
btn.find('span').text(action) btn.find('span').text(action)
@subscription_status.find('>div').toggleClass('hidden') @subscription_status.find('>div').toggleClass('hidden')
if btn.attr('data-original-title')
btn.tooltip('hide')
.attr('data-original-title', action)
.tooltip('fixTitle')
...@@ -122,6 +122,9 @@ class @UserTabs ...@@ -122,6 +122,9 @@ class @UserTabs
@parentEl.find(tabSelector).html(data.html) @parentEl.find(tabSelector).html(data.html)
@loaded[action] = true @loaded[action] = true
# Fix tooltips
gl.utils.localTimeAgo($('.js-timeago', tabSelector))
loadActivities: (source) -> loadActivities: (source) ->
return if @loaded['activity'] is true return if @loaded['activity'] is true
......
...@@ -79,6 +79,23 @@ ...@@ -79,6 +79,23 @@
@include btn-color($white-light, $border-color, $white-normal, $border-white-normal, $white-dark, $border-white-dark, $btn-white-active); @include btn-color($white-light, $border-color, $white-normal, $border-white-normal, $white-dark, $border-white-dark, $btn-white-active);
} }
@mixin btn-with-margin {
margin-left: $btn-side-margin;
float: left;
&.inline {
float: none;
}
&.btn-sm {
margin-left: $btn-sm-side-margin;
}
&.btn-xs {
margin-left: $btn-xs-side-margin;
}
}
.btn { .btn {
@include btn-default; @include btn-default;
@include btn-white; @include btn-white;
...@@ -142,15 +159,9 @@ ...@@ -142,15 +159,9 @@
} }
&.btn-grouped { &.btn-grouped {
margin-right: 7px; @include btn-with-margin;
float: left;
&:last-child {
margin-right: 0;
}
&.btn-xs {
margin-right: 3px;
}
} }
&.disabled { &.disabled {
pointer-events: auto !important; pointer-events: auto !important;
} }
...@@ -192,11 +203,7 @@ ...@@ -192,11 +203,7 @@
.btn-group { .btn-group {
&.btn-grouped { &.btn-grouped {
margin-right: 7px; @include btn-with-margin;
float: left;
&:last-child {
margin-right: 0;
}
} }
} }
......
...@@ -124,6 +124,7 @@ ...@@ -124,6 +124,7 @@
position: relative; position: relative;
padding: 5px 10px; padding: 5px 10px;
color: $dropdown-link-color; color: $dropdown-link-color;
line-height: initial;
text-overflow: ellipsis; text-overflow: ellipsis;
border-radius: 2px; border-radius: 2px;
white-space: nowrap; white-space: nowrap;
......
...@@ -76,6 +76,7 @@ label { ...@@ -76,6 +76,7 @@ label {
.form-control { .form-control {
@include box-shadow(none); @include box-shadow(none);
border-radius: 3px; border-radius: 3px;
padding: $gl-vert-padding $gl-input-padding;
} }
.select-wrapper { .select-wrapper {
......
...@@ -22,17 +22,17 @@ ...@@ -22,17 +22,17 @@
&:hover { &:hover {
background-color: $color-dark; background-color: $color-dark;
a { a {
color: #fff; color: $white-light;
h3 { h3 {
color: #fff; color: $white-light;
} }
} }
} }
} }
.collapse-nav a { .collapse-nav a {
color: #fff; color: $white-light;
background: $color; background: $color;
} }
...@@ -45,7 +45,7 @@ ...@@ -45,7 +45,7 @@
&:hover { &:hover {
background-color: $color-dark; background-color: $color-dark;
color: #fff; color: $white-light;
text-decoration: none; text-decoration: none;
} }
} }
...@@ -63,10 +63,20 @@ ...@@ -63,10 +63,20 @@
color: $color-light; color: $color-light;
} }
path,
polygon {
fill: $color-light;
}
.count { .count {
color: $color-light; color: $color-light;
background: $color-dark; background: $color-dark;
} }
svg {
position: relative;
top: 3px;
}
} }
&.separate-item { &.separate-item {
...@@ -74,7 +84,7 @@ ...@@ -74,7 +84,7 @@
} }
&.active a { &.active a {
color: #fff; color: $white-light;
background: $color-dark; background: $color-dark;
&.no-highlight { &.no-highlight {
...@@ -82,7 +92,12 @@ ...@@ -82,7 +92,12 @@
} }
i { i {
color: #fff color: $white-light
}
path,
polygon {
fill: $white-light;
} }
} }
} }
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
font-family: $regular_font; font-family: $regular_font;
font-size: $font-size-base; font-size: $font-size-base;
&.ui-datepicker,
&.ui-datepicker-inline { &.ui-datepicker-inline {
border: 1px solid #ddd; border: 1px solid #ddd;
padding: 10px; padding: 10px;
...@@ -10,6 +11,25 @@ ...@@ -10,6 +11,25 @@
.ui-datepicker-header { .ui-datepicker-header {
background: #fff; background: #fff;
border-color: #ddd; border-color: #ddd;
.ui-datepicker-prev,
.ui-datepicker-next {
top: 4px;
}
.ui-datepicker-prev {
left: 2px;
}
.ui-datepicker-next {
right: 2px;
}
.ui-state-hover {
background: transparent;
border: 0;
cursor: pointer;
}
} }
.ui-datepicker-calendar td a { .ui-datepicker-calendar td a {
...@@ -36,21 +56,18 @@ ...@@ -36,21 +56,18 @@
} }
.ui-state-highlight { .ui-state-highlight {
border: 1px solid #eee; border: 0;
background: #eee; background: transparent;
} }
.ui-state-active { .ui-datepicker-calendar {
.ui-state-active,
.ui-state-hover,
.ui-state-focus {
border: 1px solid $gl-primary; border: 1px solid $gl-primary;
background: $gl-primary; background: $gl-primary;
color: #fff; color: #fff;
} }
.ui-state-hover,
.ui-state-focus {
border: 1px solid $row-hover;
background: $row-hover;
color: #333;
} }
} }
......
...@@ -137,8 +137,16 @@ ul.content-list { ...@@ -137,8 +137,16 @@ ul.content-list {
padding-top: 1px; padding-top: 1px;
float: right; float: right;
.btn { > .btn,
padding: 10px 14px; > .btn-group {
margin-right: $gl-padding-top;
display: inline-block;
margin-top: 4px;
margin-bottom: 4px;
&:last-child {
margin-right: 0;
}
} }
} }
......
...@@ -171,6 +171,7 @@ ...@@ -171,6 +171,7 @@
> form { > form {
display: inline-block; display: inline-block;
margin-top: -1px; margin-top: -1px;
margin-bottom: 12px;
} }
.icon-label { .icon-label {
...@@ -207,7 +208,7 @@ ...@@ -207,7 +208,7 @@
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
padding-bottom: 0; padding-bottom: 0;
width: 100%;
.btn, form, .dropdown, .dropdown-menu-toggle, .form-control { .btn, form, .dropdown, .dropdown-menu-toggle, .form-control {
margin: 0 0 10px; margin: 0 0 10px;
display: block; display: block;
...@@ -238,16 +239,6 @@ ...@@ -238,16 +239,6 @@
margin: 0; margin: 0;
} }
} }
/* Small devices (tablets, 768px and lower) */
@media (max-width: $screen-sm-max) {
width: 100%;
text-align: left;
input {
width: 300px;
}
}
} }
} }
...@@ -304,6 +295,19 @@ ...@@ -304,6 +295,19 @@
border-bottom: none; border-bottom: none;
height: 51px; height: 51px;
svg {
position: relative;
top: 2px;
margin-right: 2px;
height: 15px;
width: auto;
path,
polygon {
fill: $layout-link-gray;
}
}
.fade-right { .fade-right {
@include fade(left, rgba(250, 250, 250, 0.4), $background-color); @include fade(left, rgba(250, 250, 250, 0.4), $background-color);
right: 0; right: 0;
...@@ -325,9 +329,17 @@ ...@@ -325,9 +329,17 @@
} }
&.active { &.active {
a, i { a, i {
color: $black; color: $black;
} }
svg {
path,
polygon {
fill: $black;
}
}
} }
.badge { .badge {
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
background: #fff; background: #fff;
border-color: $input-border; border-color: $input-border;
height: 35px; height: 35px;
padding: $gl-vert-padding $gl-btn-padding; padding: $gl-vert-padding $gl-input-padding;
font-size: $gl-font-size; font-size: $gl-font-size;
line-height: 1.42857143; line-height: 1.42857143;
border-radius: $border-radius-base; border-radius: $border-radius-base;
......
...@@ -38,6 +38,11 @@ ...@@ -38,6 +38,11 @@
.header-logo { .header-logo {
height: $header-height; height: $header-height;
padding: 8px 26px; padding: 8px 26px;
width: $sidebar_width;
position: fixed;
z-index: 999;
overflow: hidden;
transition-duration: .3s;
&:hover { &:hover {
background-color: #eee; background-color: #eee;
...@@ -73,7 +78,8 @@ ...@@ -73,7 +78,8 @@
.nav-sidebar { .nav-sidebar {
margin: 22px 0; margin-top: 22 + $header-height;
margin-bottom: 116px;
transition-duration: .3s; transition-duration: .3s;
list-style: none; list-style: none;
overflow: hidden; overflow: hidden;
...@@ -109,8 +115,7 @@ ...@@ -109,8 +115,7 @@
} }
i { i {
width: 16px; font-size: 16px;
color: $gray-light;
} }
.nav-link-text { .nav-link-text {
...@@ -167,6 +172,7 @@ ...@@ -167,6 +172,7 @@
.header-logo { .header-logo {
width: 0; width: 0;
padding: 8px 0;
a { a {
padding-left: ($sidebar_collapsed_width - 36) / 2; padding-left: ($sidebar_collapsed_width - 36) / 2;
......
...@@ -192,3 +192,8 @@ ...@@ -192,3 +192,8 @@
.text-info:hover { .text-info:hover {
color: $brand-info; color: $brand-info;
} }
// Prevent datetimes on tooltips to break into two lines
.local-timeago {
white-space: nowrap;
}
...@@ -57,6 +57,7 @@ $code_line_height: 1.5; ...@@ -57,6 +57,7 @@ $code_line_height: 1.5;
*/ */
$gl-padding: 16px; $gl-padding: 16px;
$gl-btn-padding: 10px; $gl-btn-padding: 10px;
$gl-input-padding: 10px;
$gl-vert-padding: 6px; $gl-vert-padding: 6px;
$gl-padding-top: 10px; $gl-padding-top: 10px;
...@@ -79,6 +80,9 @@ $provider-btn-not-active-color: #4688f1; ...@@ -79,6 +80,9 @@ $provider-btn-not-active-color: #4688f1;
$link-underline-blue: #4a8bee; $link-underline-blue: #4a8bee;
$layout-link-gray: #7e7c7c; $layout-link-gray: #7e7c7c;
$todo-alert-blue: #428bca; $todo-alert-blue: #428bca;
$btn-side-margin: 10px;
$btn-sm-side-margin: 7px;
$btn-xs-side-margin: 5px;
/* /*
* Color schema * Color schema
...@@ -121,7 +125,7 @@ $border-white-normal: #d6dae2; ...@@ -121,7 +125,7 @@ $border-white-normal: #d6dae2;
$border-white-dark: #c6cacf; $border-white-dark: #c6cacf;
$border-gray-light: #dcdcdc; $border-gray-light: #dcdcdc;
$border-gray-normal: rgba(0, 0, 0, 0.10); $border-gray-normal: #d7d7d7;
$border-gray-dark: #c6cacf; $border-gray-dark: #c6cacf;
$border-green-light: #2faa60; $border-green-light: #2faa60;
......
...@@ -2,13 +2,21 @@ ...@@ -2,13 +2,21 @@
margin-bottom: 20px; margin-bottom: 20px;
border-bottom: 1px solid #eee; border-bottom: 1px solid #eee;
> h1 { > h1, h2, h3, h4, h5, h6 {
font-weight: 400; font-weight: 400;
} }
.lead { .lead {
margin-bottom: 20px; margin-bottom: 20px;
} }
ul, ol {
padding-left: 0;
}
li {
list-style-type: none;
}
} }
.confirmation-content { .confirmation-content {
......
...@@ -50,11 +50,26 @@ ...@@ -50,11 +50,26 @@
.label-row { .label-row {
.label-name { .label-name {
display: block;
margin-bottom: 10px;
@media (min-width: $screen-sm-min) {
display: inline-block; display: inline-block;
width: 170px; width: 200px;
margin-bottom: 0;
}
}
@media (max-width: $screen-xs-min) { .label-description {
display: block; display: block;
margin-bottom: 10px;
@media (min-width: $screen-sm-min) {
display: inline-block;
width: 40%;
margin-left: 10px;
margin-bottom: 0;
vertical-align: middle;
} }
} }
...@@ -68,10 +83,6 @@ ...@@ -68,10 +83,6 @@
padding: 3px 4px; padding: 3px 4px;
} }
.label-subscription {
display: inline-block;
}
.dropdown-labels-error { .dropdown-labels-error {
padding: 5px 10px; padding: 5px 10px;
margin-bottom: 10px; margin-bottom: 10px;
...@@ -79,62 +90,27 @@ ...@@ -79,62 +90,27 @@
color: $white-light; color: $white-light;
} }
@mixin labels-mobile {
@media (max-width: $screen-xs-min) {
display: block;
width: 100%;
margin-left: 0;
padding: 10px 0;
}
}
.manage-labels-list { .manage-labels-list {
.btn-action {
color: $gl-dark-link-color;
.prepend-left-10, .prepend-description-left { .fa {
display: inline-block; font-size: 18px;
width: 40%;
vertical-align: middle; vertical-align: middle;
@include labels-mobile;
} }
.prepend-description-left { &:hover {
width: 57%; color: $gl-link-color;
@include labels-mobile;
}
.pull-info-right {
float: right;
@media (max-width: $screen-xs-min) { &.remove-row {
float: none; color: $gl-danger;
} }
.action-buttons {
border-color: transparent;
padding: 6px;
color: $gl-text-color;
&.label-subscribe-button {
padding-left: 0;
} }
} }
i { .dropdown {
color: $gl-text-color; @media (min-width: $screen-sm-min) {
} float: right;
.append-right-20 {
a {
color: $gl-text-color;
}
@media (max-width: $screen-xs-min) {
display: block;
margin-bottom: 10px;
}
} }
} }
} }
...@@ -169,3 +145,40 @@ ...@@ -169,3 +145,40 @@
} }
} }
} }
.filtered-labels {
.label-row {
&:not(:last-child) {
margin-right: 5px;
}
}
.label-remove {
border-left: 1px solid rgba(0, 0, 0, .1);
z-index: 3;
}
.btn {
color: inherit;
}
}
.label-options-toggle {
width: 100%;
}
.label-subscribe-button {
.label-subscribe-button-loading {
display: none;
}
&.disabled {
.label-subscribe-button-icon {
display: none;
}
.label-subscribe-button-loading {
display: block;
}
}
}
...@@ -108,6 +108,11 @@ ...@@ -108,6 +108,11 @@
font-size: 17px; font-size: 17px;
margin: 5px 0; margin: 5px 0;
color: $gl-gray-dark; color: $gl-gray-dark;
&.has-conflicts .fa-exclamation-triangle {
color: $gl-warning;
}
} }
p:last-child { p:last-child {
......
...@@ -129,17 +129,8 @@ ...@@ -129,17 +129,8 @@
display: none; display: none;
font-size: 15px; font-size: 15px;
.form-actions { .md-area {
padding-left: 20px; background-color: #fff;
.btn-save {
float: left;
}
.note-form-option {
float: left;
padding: 2px 0 0 25px;
}
} }
} }
......
...@@ -74,6 +74,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -74,6 +74,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:two_factor_grace_period, :two_factor_grace_period,
:gravatar_enabled, :gravatar_enabled,
:sign_in_text, :sign_in_text,
:after_sign_up_text,
:help_page_text, :help_page_text,
:home_page_url, :home_page_url,
:after_sign_out_path, :after_sign_out_path,
......
...@@ -42,46 +42,8 @@ class JwtController < ApplicationController ...@@ -42,46 +42,8 @@ class JwtController < ApplicationController
end end
def authenticate_user(login, password) def authenticate_user(login, password)
# TODO: this is a copy and paste from grack_auth, user = Gitlab::Auth.find_in_gitlab_or_ldap(login, password)
# it should be refactored in the future Gitlab::Auth.rate_limit!(request.ip, success: user.present?, login: login)
user = Gitlab::Auth.new.find(login, password)
# If the user authenticated successfully, we reset the auth failure count
# from Rack::Attack for that IP. A client may attempt to authenticate
# with a username and blank password first, and only after it receives
# a 401 error does it present a password. Resetting the count prevents
# false positives from occurring.
#
# Otherwise, we let Rack::Attack know there was a failed authentication
# attempt from this IP. This information is stored in the Rails cache
# (Redis) and will be used by the Rack::Attack middleware to decide
# whether to block requests from this IP.
config = Gitlab.config.rack_attack.git_basic_auth
if config.enabled
if user
# A successful login will reset the auth failure count from this IP
Rack::Attack::Allow2Ban.reset(request.ip, config)
else
banned = Rack::Attack::Allow2Ban.filter(request.ip, config) do
# Unless the IP is whitelisted, return true so that Allow2Ban
# increments the counter (stored in Rails.cache) for the IP
if config.ip_whitelist.include?(request.ip)
false
else
true
end
end
if banned
Rails.logger.info "IP #{request.ip} failed to login " \
"as #{login} but has been temporarily banned from Git auth"
return
end
end
end
user user
end end
end end
...@@ -32,7 +32,7 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController ...@@ -32,7 +32,7 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
def verify_user_oauth_applications_enabled def verify_user_oauth_applications_enabled
return if current_application_settings.user_oauth_applications? return if current_application_settings.user_oauth_applications?
redirect_to applications_profile_url redirect_to profile_path
end end
def set_index_vars def set_index_vars
......
class Projects::GitHttpController < Projects::ApplicationController
attr_reader :user
# Git clients will not know what authenticity token to send along
skip_before_action :verify_authenticity_token
skip_before_action :repository
before_action :authenticate_user
before_action :ensure_project_found!
# GET /foo/bar.git/info/refs?service=git-upload-pack (git pull)
# GET /foo/bar.git/info/refs?service=git-receive-pack (git push)
def info_refs
if upload_pack? && upload_pack_allowed?
render_ok
elsif receive_pack? && receive_pack_allowed?
render_ok
else
render_not_found
end
end
# POST /foo/bar.git/git-upload-pack (git pull)
def git_upload_pack
if upload_pack? && upload_pack_allowed?
render_ok
else
render_not_found
end
end
# POST /foo/bar.git/git-receive-pack" (git push)
def git_receive_pack
if receive_pack? && receive_pack_allowed?
render_ok
else
render_not_found
end
end
private
def authenticate_user
return if project && project.public? && upload_pack?
authenticate_or_request_with_http_basic do |login, password|
auth_result = Gitlab::Auth.find(login, password, project: project, ip: request.ip)
if auth_result.type == :ci && upload_pack?
@ci = true
elsif auth_result.type == :oauth && !upload_pack?
# Not allowed
else
@user = auth_result.user
end
ci? || user
end
end
def ensure_project_found!
render_not_found if project.blank?
end
def project
return @project if defined?(@project)
project_id, _ = project_id_with_suffix
if project_id.blank?
@project = nil
else
@project = Project.find_with_namespace("#{params[:namespace_id]}/#{project_id}")
end
end
# This method returns two values so that we can parse
# params[:project_id] (untrusted input!) in exactly one place.
def project_id_with_suffix
id = params[:project_id] || ''
%w[.wiki.git .git].each do |suffix|
if id.end_with?(suffix)
# Be careful to only remove the suffix from the end of 'id'.
# Accidentally removing it from the middle is how security
# vulnerabilities happen!
return [id.slice(0, id.length - suffix.length), suffix]
end
end
# Something is wrong with params[:project_id]; do not pass it on.
[nil, nil]
end
def upload_pack?
git_command == 'git-upload-pack'
end
def receive_pack?
git_command == 'git-receive-pack'
end
def git_command
if action_name == 'info_refs'
params[:service]
else
action_name.dasherize
end
end
def render_ok
render json: Gitlab::Workhorse.git_http_ok(repository, user)
end
def repository
_, suffix = project_id_with_suffix
if suffix == '.wiki.git'
project.wiki.repository
else
project.repository
end
end
def render_not_found
render text: 'Not Found', status: :not_found
end
def ci?
@ci.present?
end
def upload_pack_allowed?
return false unless Gitlab.config.gitlab_shell.upload_pack
if user
Gitlab::GitAccess.new(user, project).download_access_check.allowed?
else
ci? || project.public?
end
end
def receive_pack_allowed?
return false unless Gitlab.config.gitlab_shell.receive_pack
# Skip user authorization on upload request.
# It will be done by the pre-receive hook in the repository.
user.present?
end
end
...@@ -95,7 +95,7 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -95,7 +95,7 @@ class Projects::WikisController < Projects::ApplicationController
ext.analyze(text, author: current_user) ext.analyze(text, author: current_user)
render json: { render json: {
body: view_context.markdown(text, pipeline: :wiki, project_wiki: @project_wiki), body: view_context.markdown(text, pipeline: :wiki, project_wiki: @project_wiki, page_slug: params[:id]),
references: { references: {
users: ext.users.map(&:username) users: ext.users.map(&:username)
} }
......
...@@ -14,6 +14,7 @@ class SessionsController < Devise::SessionsController ...@@ -14,6 +14,7 @@ class SessionsController < Devise::SessionsController
before_action :load_recaptcha before_action :load_recaptcha
def new def new
set_minimum_password_length
if Gitlab.config.ldap.enabled if Gitlab.config.ldap.enabled
@ldap_servers = Gitlab::LDAP::Config.servers @ldap_servers = Gitlab::LDAP::Config.servers
else else
......
...@@ -30,4 +30,8 @@ module AppearancesHelper ...@@ -30,4 +30,8 @@ module AppearancesHelper
render 'shared/logo.svg' render 'shared/logo.svg'
end end
end end
def navbar_icon(icon_name)
render "shared/icons/#{icon_name}.svg"
end
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 after_sign_up_text
current_application_settings.after_sign_up_text
end
def shared_runners_text def shared_runners_text
current_application_settings.shared_runners_text current_application_settings.shared_runners_text
end end
......
...@@ -108,7 +108,7 @@ module GitlabMarkdownHelper ...@@ -108,7 +108,7 @@ module GitlabMarkdownHelper
def render_wiki_content(wiki_page) def render_wiki_content(wiki_page)
case wiki_page.format case wiki_page.format
when :markdown when :markdown
markdown(wiki_page.content, pipeline: :wiki, project_wiki: @project_wiki) markdown(wiki_page.content, pipeline: :wiki, project_wiki: @project_wiki, page_slug: wiki_page.slug)
when :asciidoc when :asciidoc
asciidoc(wiki_page.content) asciidoc(wiki_page.content)
else else
......
...@@ -32,7 +32,7 @@ module LabelsHelper ...@@ -32,7 +32,7 @@ module LabelsHelper
# link_to_label(label) { "My Custom Label Text" } # link_to_label(label) { "My Custom Label Text" }
# #
# Returns a String # Returns a String
def link_to_label(label, project: nil, type: :issue, tooltip: true, &block) def link_to_label(label, project: nil, type: :issue, tooltip: true, css_class: nil, &block)
project ||= @project || label.project project ||= @project || label.project
link = send("namespace_project_#{type.to_s.pluralize}_path", link = send("namespace_project_#{type.to_s.pluralize}_path",
project.namespace, project.namespace,
...@@ -40,9 +40,9 @@ module LabelsHelper ...@@ -40,9 +40,9 @@ module LabelsHelper
label_name: [label.name]) label_name: [label.name])
if block_given? if block_given?
link_to link, &block link_to link, class: css_class, &block
else else
link_to render_colored_label(label, tooltip: tooltip), link link_to render_colored_label(label, tooltip: tooltip), link, class: css_class
end end
end end
......
...@@ -56,7 +56,7 @@ module MilestonesHelper ...@@ -56,7 +56,7 @@ module MilestonesHelper
def milestone_remaining_days(milestone) def milestone_remaining_days(milestone)
if milestone.expired? if milestone.expired?
content_tag(:strong, 'expired') content_tag(:strong, 'Past due')
elsif milestone.due_date elsif milestone.due_date
days = milestone.remaining_days days = milestone.remaining_days
content = content_tag(:strong, days) content = content_tag(:strong, days)
......
...@@ -113,7 +113,10 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -113,7 +113,10 @@ class ApplicationSetting < ActiveRecord::Base
signup_enabled: Settings.gitlab['signup_enabled'], signup_enabled: Settings.gitlab['signup_enabled'],
signin_enabled: Settings.gitlab['signin_enabled'], signin_enabled: Settings.gitlab['signin_enabled'],
gravatar_enabled: Settings.gravatar['enabled'], gravatar_enabled: Settings.gravatar['enabled'],
sign_in_text: Settings.extra['sign_in_text'], sign_in_text: nil,
after_sign_up_text: nil,
help_page_text: nil,
shared_runners_text: nil,
restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'], restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'],
max_attachment_size: Settings.gitlab['max_attachment_size'], max_attachment_size: Settings.gitlab['max_attachment_size'],
session_expire_delay: Settings.gitlab['session_expire_delay'], session_expire_delay: Settings.gitlab['session_expire_delay'],
......
...@@ -198,7 +198,7 @@ class Commit ...@@ -198,7 +198,7 @@ class Commit
end end
def notes_with_associations def notes_with_associations
notes.includes(:author, :project) notes.includes(:author)
end end
def method_missing(m, *args, &block) def method_missing(m, *args, &block)
......
...@@ -17,7 +17,12 @@ module Issuable ...@@ -17,7 +17,12 @@ module Issuable
belongs_to :assignee, class_name: "User" belongs_to :assignee, class_name: "User"
belongs_to :updated_by, class_name: "User" belongs_to :updated_by, class_name: "User"
belongs_to :milestone belongs_to :milestone
has_many :notes, as: :noteable, dependent: :destroy has_many :notes, as: :noteable, dependent: :destroy do
def authors_loaded?
# We check first if we're loaded to not load unnecesarily.
loaded? && to_a.all? { |note| note.association(:author).loaded? }
end
end
has_many :label_links, as: :target, dependent: :destroy has_many :label_links, as: :target, dependent: :destroy
has_many :labels, through: :label_links has_many :labels, through: :label_links
has_many :todos, as: :target, dependent: :destroy has_many :todos, as: :target, dependent: :destroy
...@@ -44,6 +49,7 @@ module Issuable ...@@ -44,6 +49,7 @@ module Issuable
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) }
scope :inc_notes_with_associations, -> { includes(notes: :author) }
scope :references_project, -> { references(:project) } scope :references_project, -> { references(:project) }
scope :non_archived, -> { join_project.where(projects: { archived: false }) } scope :non_archived, -> { join_project.where(projects: { archived: false }) }
...@@ -179,8 +185,14 @@ module Issuable ...@@ -179,8 +185,14 @@ module Issuable
end end
def user_notes_count def user_notes_count
if notes.loaded?
# Use the in-memory association to select and count to avoid hitting the db
notes.to_a.count { |note| !note.system? }
else
# do the count query
notes.user.count notes.user.count
end end
end
def subscribed_without_subscriptions?(user) def subscribed_without_subscriptions?(user)
participants(user).include?(user) participants(user).include?(user)
...@@ -239,7 +251,13 @@ module Issuable ...@@ -239,7 +251,13 @@ module Issuable
end end
def notes_with_associations def notes_with_associations
notes.includes(:author, :project) # If A has_many Bs, and B has_many Cs, and you do
# `A.includes(b: :c).each { |a| a.b.includes(:c) }`, sadly ActiveRecord
# will do the inclusion again. So, we check if all notes in the relation
# already have their authors loaded (possibly because the scope
# `inc_notes_with_associations` was used) and skip the inclusion if that's
# the case.
notes.authors_loaded? ? notes : notes.includes(:author)
end end
def updated_tasks def updated_tasks
......
...@@ -102,7 +102,7 @@ class Snippet < ActiveRecord::Base ...@@ -102,7 +102,7 @@ class Snippet < ActiveRecord::Base
end end
def notes_with_associations def notes_with_associations
notes.includes(:author, :project) notes.includes(:author)
end end
class << self class << self
......
...@@ -20,7 +20,7 @@ class TodoService ...@@ -20,7 +20,7 @@ class TodoService
# * mark all pending todos related to the issue for the current user as done # * mark all pending todos related to the issue for the current user as done
# #
def update_issue(issue, current_user) def update_issue(issue, current_user)
create_mention_todos(issue.project, issue, current_user) update_issuable(issue, current_user)
end end
# When close an issue we should: # When close an issue we should:
...@@ -53,7 +53,7 @@ class TodoService ...@@ -53,7 +53,7 @@ class TodoService
# * create a todo for each mentioned user on merge request # * create a todo for each mentioned user on merge request
# #
def update_merge_request(merge_request, current_user) def update_merge_request(merge_request, current_user)
create_mention_todos(merge_request.project, merge_request, current_user) update_issuable(merge_request, current_user)
end end
# When close a merge request we should: # When close a merge request we should:
...@@ -153,6 +153,13 @@ class TodoService ...@@ -153,6 +153,13 @@ class TodoService
create_mention_todos(issuable.project, issuable, author) create_mention_todos(issuable.project, issuable, author)
end end
def update_issuable(issuable, author)
# Skip toggling a task list item in a description
return if issuable.tasks? && issuable.updated_tasks.any?
create_mention_todos(issuable.project, issuable, author)
end
def handle_note(note, author) def handle_note(note, author)
# Skip system notes, and notes on project snippet # Skip system notes, and notes on project snippet
return if note.system? || note.for_snippet? return if note.system? || note.for_snippet?
......
...@@ -154,6 +154,11 @@ ...@@ -154,6 +154,11 @@
.col-sm-10 .col-sm-10
= f.text_area :sign_in_text, class: 'form-control', rows: 4 = f.text_area :sign_in_text, class: 'form-control', rows: 4
.help-block Markdown enabled .help-block Markdown enabled
.form-group
= f.label :after_sign_up_text, class: 'control-label col-sm-2'
.col-sm-10
= f.text_area :after_sign_up_text, class: 'form-control', rows: 4
.help-block Markdown enabled
.form-group .form-group
= f.label :help_page_text, class: 'control-label col-sm-2' = f.label :help_page_text, class: 'control-label col-sm-2'
.col-sm-10 .col-sm-10
......
...@@ -7,9 +7,6 @@ ...@@ -7,9 +7,6 @@
= awards.count = awards.count
- if current_user - if current_user
:javascript
gl.awardMenuUrl = "#{emojis_path}"
.award-menu-holder.js-award-holder .award-menu-holder.js-award-holder
%button.btn.award-control.js-add-award{ type: "button" } %button.btn.award-control.js-add-award{ type: "button" }
= icon('smile-o', class: "award-control-icon award-control-icon-normal") = icon('smile-o', class: "award-control-icon award-control-icon-normal")
......
...@@ -9,5 +9,4 @@ ...@@ -9,5 +9,4 @@
- if current_user.can_create_group? - if current_user.can_create_group?
.nav-controls .nav-controls
= link_to new_group_path, class: "btn btn-new" do = link_to new_group_path, class: "btn btn-new" do
= icon('plus')
New Group New Group
...@@ -18,5 +18,4 @@ ...@@ -18,5 +18,4 @@
= render 'shared/projects/dropdown' = render 'shared/projects/dropdown'
- if current_user.can_create_project? - if current_user.can_create_project?
= link_to new_project_path, class: 'btn btn-new' do = link_to new_project_path, class: 'btn btn-new' do
= icon('plus')
New Project New Project
...@@ -3,6 +3,9 @@ ...@@ -3,6 +3,9 @@
Almost there... Almost there...
%p.lead %p.lead
Please check your email to confirm your account Please check your email to confirm your account
- if after_sign_up_text.present?
.well-confirmation.text-center
= markdown(after_sign_up_text)
%p.confirmation-content.text-center %p.confirmation-content.text-center
No confirmation email received? Please check your spam folder or No confirmation email received? Please check your spam folder or
.append-bottom-20.prepend-top-20.text-center .append-bottom-20.prepend-top-20.text-center
......
...@@ -6,7 +6,8 @@ ...@@ -6,7 +6,8 @@
- if @user.two_factor_otp_enabled? - if @user.two_factor_otp_enabled?
%h5 Authenticate via Two-Factor App %h5 Authenticate via Two-Factor App
= form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f| = form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f|
= f.hidden_field :remember_me, value: params[resource_name][:remember_me] - resource_params = params[resource_name].presence || params
= f.hidden_field :remember_me, value: resource_params.fetch(:remember_me, 0)
= f.text_field :otp_attempt, class: 'form-control', placeholder: 'Two-Factor Authentication code', required: true, autofocus: true, autocomplete: 'off' = f.text_field :otp_attempt, class: 'form-control', placeholder: 'Two-Factor Authentication code', required: true, autofocus: true, autocomplete: 'off'
%p.help-block.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes. %p.help-block.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes.
.prepend-top-20 .prepend-top-20
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
%div %div
= f.email_field :email, class: "form-control middle", placeholder: "Email", required: true = f.email_field :email, class: "form-control middle", placeholder: "Email", required: true
.form-group.append-bottom-20#password-strength .form-group.append-bottom-20#password-strength
= f.password_field :password, class: "form-control bottom", placeholder: "Password", required: true = f.password_field :password, class: "form-control bottom", placeholder: "Password - minimum length #{@minimum_password_length} characters", required: true, pattern: ".{#{@minimum_password_length},}", title: "Minimum length is #{@minimum_password_length} characters"
%div %div
- if current_application_settings.recaptcha_enabled - if current_application_settings.recaptcha_enabled
= recaptcha_tags = recaptcha_tags
......
...@@ -34,9 +34,9 @@ ...@@ -34,9 +34,9 @@
%strong.member-access-level= member.human_access %strong.member-access-level= member.human_access
- if show_controls - if show_controls
- if can?(current_user, :update_group_member, member) - if can?(current_user, :update_group_member, member)
= button_tag class: "btn-xs btn js-toggle-button", = button_tag class: "btn-xs btn btn-grouped inline js-toggle-button",
title: 'Edit access level', type: 'button' do title: 'Edit access level', type: 'button' do
%i.fa.fa-pencil-square-o = icon('pencil')
- if can?(current_user, :destroy_group_member, member) - if can?(current_user, :destroy_group_member, member)
&nbsp; &nbsp;
...@@ -46,7 +46,7 @@ ...@@ -46,7 +46,7 @@
Leave Leave
- else - else
= link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do = link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
%i.fa.fa-minus.fa-inverse = icon('trash')
.edit-member.hide.js-toggle-content .edit-member.hide.js-toggle-content
%br %br
......
...@@ -39,9 +39,8 @@ ...@@ -39,9 +39,8 @@
.col-md-6 .col-md-6
.form-group .form-group
= f.label :due_date, "Due Date", class: "control-label" = f.label :due_date, "Due Date", class: "control-label"
.col-sm-10= f.hidden_field :due_date
.col-sm-10 .col-sm-10
.datepicker = f.text_field :due_date, class: "datepicker form-control", placeholder: "Select due date"
.form-actions .form-actions
= f.submit 'Create Milestone', class: "btn-create btn" = f.submit 'Create Milestone', class: "btn-create btn"
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
= auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity") = auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity")
.cover-block.groups-cover-block .cover-block.groups-cover-block
.container-fluid.container-limited %div{ class: (container_class) }
= link_to group_icon(@group), target: '_blank' do = link_to group_icon(@group), target: '_blank' do
= image_tag group_icon(@group), class: "avatar group-avatar s70" = image_tag group_icon(@group), class: "avatar group-avatar s70"
.group-info .group-info
...@@ -35,7 +35,6 @@ ...@@ -35,7 +35,6 @@
= render 'shared/projects/dropdown' = render 'shared/projects/dropdown'
- if can? current_user, :create_projects, @group - if can? current_user, :create_projects, @group
= link_to new_project_path(namespace_id: @group.id), class: 'btn btn-new pull-right' do = link_to new_project_path(namespace_id: @group.id), class: 'btn btn-new pull-right' do
= icon('plus')
New Project New Project
.tab-content .tab-content
......
...@@ -2,106 +2,102 @@ ...@@ -2,106 +2,102 @@
= nav_link(controller: :dashboard, html_options: {class: 'home'}) do = nav_link(controller: :dashboard, html_options: {class: 'home'}) do
= link_to admin_root_path, title: 'Overview' do = link_to admin_root_path, title: 'Overview' do
= icon('dashboard fw') = icon('dashboard fw')
%span .nav-link-text
Overview Overview
= nav_link(controller: [:admin, :projects]) do = nav_link(controller: [:admin, :projects]) do
= link_to admin_namespaces_projects_path, title: 'Projects' do = link_to admin_namespaces_projects_path, title: 'Projects' do
= icon('cube fw') = icon('cube fw')
%span .nav-link-text
Projects Projects
= nav_link(controller: :users) do = nav_link(controller: :users) do
= link_to admin_users_path, title: 'Users' do = link_to admin_users_path, title: 'Users' do
= icon('user fw') = icon('user fw')
%span .nav-link-text
Users Users
= nav_link(controller: :groups) do = nav_link(controller: :groups) do
= link_to admin_groups_path, title: 'Groups' do = link_to admin_groups_path, title: 'Groups' do
= icon('group fw') = icon('group fw')
%span .nav-link-text
Groups Groups
= nav_link(controller: :deploy_keys) do = nav_link(controller: :deploy_keys) do
= link_to admin_deploy_keys_path, title: 'Deploy Keys' do = link_to admin_deploy_keys_path, title: 'Deploy Keys' do
= icon('key fw') = icon('key fw')
%span .nav-link-text
Deploy Keys Deploy Keys
= nav_link path: ['runners#index', 'runners#show'] do = nav_link path: ['runners#index', 'runners#show'] do
= link_to admin_runners_path, title: 'Runners' do = link_to admin_runners_path, title: 'Runners' do
= icon('cog fw') = icon('cog fw')
%span .nav-link-text
Runners Runners
%span.count= number_with_delimiter(Ci::Runner.count(:all))
= nav_link path: 'builds#index' do = nav_link path: 'builds#index' do
= link_to admin_builds_path, title: 'Builds' do = link_to admin_builds_path, title: 'Builds' do
= icon('link fw') = icon('link fw')
%span .nav-link-text
Builds Builds
%span.count= number_with_delimiter(Ci::Build.count(:all))
= nav_link(controller: :logs) do = nav_link(controller: :logs) do
= link_to admin_logs_path, title: 'Logs' do = link_to admin_logs_path, title: 'Logs' do
= icon('file-text fw') = icon('file-text fw')
%span .nav-link-text
Logs Logs
= nav_link(controller: :health_check) do = nav_link(controller: :health_check) do
= link_to admin_health_check_path, title: 'Health Check' do = link_to admin_health_check_path, title: 'Health Check' do
= icon('medkit fw') = icon('medkit fw')
%span .nav-link-text
Health Check Health Check
= nav_link(controller: :broadcast_messages) do = nav_link(controller: :broadcast_messages) do
= link_to admin_broadcast_messages_path, title: 'Messages' do = link_to admin_broadcast_messages_path, title: 'Messages' do
= icon('bullhorn fw') = icon('bullhorn fw')
%span .nav-link-text
Messages Messages
= nav_link(controller: :hooks) do = nav_link(controller: :hooks) do
= link_to admin_hooks_path, title: 'Hooks' do = link_to admin_hooks_path, title: 'Hooks' do
= icon('external-link fw') = icon('external-link fw')
%span .nav-link-text
Hooks Hooks
= nav_link(controller: :background_jobs) do = nav_link(controller: :background_jobs) do
= link_to admin_background_jobs_path, title: 'Background Jobs' do = link_to admin_background_jobs_path, title: 'Background Jobs' do
= icon('cog fw') = icon('cog fw')
%span .nav-link-text
Background Jobs Background Jobs
= nav_link(controller: :appearances) do = nav_link(controller: :appearances) do
= link_to admin_appearances_path, title: 'Appearances' do = link_to admin_appearances_path, title: 'Appearances' do
= icon('image') = icon('image')
%span .nav-link-text
Appearance Appearance
= nav_link(controller: :applications) do = nav_link(controller: :applications) do
= link_to admin_applications_path, title: 'Applications' do = link_to admin_applications_path, title: 'Applications' do
= icon('cloud fw') = icon('cloud fw')
%span .nav-link-text
Applications Applications
= nav_link(controller: :services) do = nav_link(controller: :services) do
= link_to admin_application_settings_services_path, title: 'Service Templates' do = link_to admin_application_settings_services_path, title: 'Service Templates' do
= icon('copy fw') = icon('copy fw')
%span .nav-link-text
Service Templates Service Templates
= nav_link(controller: :labels) do = nav_link(controller: :labels) do
= link_to admin_labels_path, title: 'Labels' do = link_to admin_labels_path, title: 'Labels' do
= icon('tags fw') = icon('tags fw')
%span .nav-link-text
Labels Labels
= nav_link(controller: :abuse_reports) do = nav_link(controller: :abuse_reports) do
= link_to admin_abuse_reports_path, title: "Abuse Reports" do = link_to admin_abuse_reports_path, title: "Abuse Reports" do
= icon('exclamation-circle fw') = icon('exclamation-circle fw')
%span .nav-link-text
Abuse Reports Abuse Reports
%span.count= number_with_delimiter(AbuseReport.count(:all))
- if askimet_enabled? - if askimet_enabled?
= nav_link(controller: :spam_logs) do = nav_link(controller: :spam_logs) do
= link_to admin_spam_logs_path, title: "Spam Logs" do = link_to admin_spam_logs_path, title: "Spam Logs" do
= icon('exclamation-triangle fw') = icon('exclamation-triangle fw')
%span .nav-link-text
Spam Logs Spam Logs
%span.count= number_with_delimiter(SpamLog.count(:all))
= nav_link(controller: :application_settings, html_options: { class: 'separate-item'}) do = nav_link(controller: :application_settings, html_options: { class: 'separate-item'}) do
= link_to admin_application_settings_path, title: 'Settings' do = link_to admin_application_settings_path, title: 'Settings' do
= icon('cogs fw') = icon('cogs fw')
%span .nav-link-text
Settings Settings
%ul.nav.nav-sidebar %ul.nav.nav-sidebar
= nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "#{project_tab_class} home"}) do = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "#{project_tab_class} home"}) do
= link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
= icon('bookmark fw') = navbar_icon('project')
.nav-link-text .nav-link-text
Projects Projects
= nav_link(controller: :todos) do = nav_link(controller: :todos) do
...@@ -11,27 +11,27 @@ ...@@ -11,27 +11,27 @@
Todos Todos
= nav_link(path: 'dashboard#activity') do = nav_link(path: 'dashboard#activity') do
= link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do
= icon('dashboard fw') = navbar_icon('activity')
.nav-link-text .nav-link-text
Activity Activity
= nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do
= link_to dashboard_groups_path, title: 'Groups' do = link_to dashboard_groups_path, title: 'Groups' do
= icon('group fw') = navbar_icon('group')
.nav-link-text .nav-link-text
Groups Groups
= nav_link(controller: 'dashboard/milestones') do = nav_link(controller: 'dashboard/milestones') do
= link_to dashboard_milestones_path, title: 'Milestones' do = link_to dashboard_milestones_path, title: 'Milestones' do
= icon('clock-o fw') = navbar_icon('milestones')
.nav-link-text .nav-link-text
Milestones Milestones
= nav_link(path: 'dashboard#issues') do = nav_link(path: 'dashboard#issues') do
= link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do
= icon('exclamation-circle fw') = navbar_icon('issues')
.nav-link-text .nav-link-text
Issues Issues
= nav_link(path: 'dashboard#merge_requests') do = nav_link(path: 'dashboard#merge_requests') do
= link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do
= icon('tasks fw') = navbar_icon('mr')
.nav-link-text .nav-link-text
Merge Requests Merge Requests
= nav_link(controller: :snippets) do = nav_link(controller: :snippets) do
......
...@@ -2,20 +2,20 @@ ...@@ -2,20 +2,20 @@
= nav_link(path: ['dashboard#show', 'root#show', 'projects#trending', 'projects#starred', 'projects#index'], html_options: {class: 'home'}) do = nav_link(path: ['dashboard#show', 'root#show', 'projects#trending', 'projects#starred', 'projects#index'], html_options: {class: 'home'}) do
= link_to explore_root_path, title: 'Projects' do = link_to explore_root_path, title: 'Projects' do
= icon('bookmark fw') = icon('bookmark fw')
%span .nav-link-text
Projects Projects
= nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do
= link_to explore_groups_path, title: 'Groups' do = link_to explore_groups_path, title: 'Groups' do
= icon('group fw') = icon('group fw')
%span .nav-link-text
Groups Groups
= nav_link(controller: :snippets) do = nav_link(controller: :snippets) do
= link_to explore_snippets_path, title: 'Snippets' do = link_to explore_snippets_path, title: 'Snippets' do
= icon('clipboard fw') = icon('clipboard fw')
%span .nav-link-text
Snippets Snippets
= nav_link(controller: :help) do = nav_link(controller: :help) do
= link_to help_path, title: 'Help' do = link_to help_path, title: 'Help' do
= icon('question-circle fw') = icon('question-circle fw')
%span .nav-link-text
Help Help
...@@ -5,36 +5,36 @@ ...@@ -5,36 +5,36 @@
.fade-left .fade-left
= nav_link(path: 'groups#show', html_options: {class: 'home'}) do = nav_link(path: 'groups#show', html_options: {class: 'home'}) do
= link_to group_path(@group), title: 'Home' do = link_to group_path(@group), title: 'Home' do
= icon('group fw') = navbar_icon('group')
%span %span
Group Group
= nav_link(path: 'groups#activity') do = nav_link(path: 'groups#activity') do
= link_to activity_group_path(@group), title: 'Activity' do = link_to activity_group_path(@group), title: 'Activity' do
= icon('dashboard fw') = navbar_icon('activity')
%span %span
Activity Activity
= nav_link(controller: [:group, :milestones]) do = nav_link(controller: [:group, :milestones]) do
= link_to group_milestones_path(@group), title: 'Milestones' do = link_to group_milestones_path(@group), title: 'Milestones' do
= icon('clock-o fw') = navbar_icon('milestones')
%span %span
Milestones Milestones
= nav_link(path: 'groups#issues') do = nav_link(path: 'groups#issues') do
= link_to issues_group_path(@group), title: 'Issues' do = link_to issues_group_path(@group), title: 'Issues' do
= icon('exclamation-circle fw') = navbar_icon('issues')
%span %span
Issues Issues
- issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute - issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute
%span.badge.count= number_with_delimiter(issues.count) %span.badge.count= number_with_delimiter(issues.count)
= nav_link(path: 'groups#merge_requests') do = nav_link(path: 'groups#merge_requests') do
= link_to merge_requests_group_path(@group), title: 'Merge Requests' do = link_to merge_requests_group_path(@group), title: 'Merge Requests' do
= icon('tasks fw') = navbar_icon('mr')
%span %span
Merge Requests Merge Requests
- merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened').execute - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened').execute
%span.badge.count= number_with_delimiter(merge_requests.count) %span.badge.count= number_with_delimiter(merge_requests.count)
= nav_link(controller: [:group_members]) do = nav_link(controller: [:group_members]) do
= link_to group_group_members_path(@group), title: 'Members' do = link_to group_group_members_path(@group), title: 'Members' do
= icon('users fw') = navbar_icon('members')
%span %span
Members Members
.fade-right .fade-right
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
= icon('gear fw') = icon('gear fw')
%span %span
Account Account
- if current_application_settings.user_oauth_applications?
= nav_link(controller: 'oauth/applications') do = nav_link(controller: 'oauth/applications') do
= link_to applications_profile_path, title: 'Applications' do = link_to applications_profile_path, title: 'Applications' do
= icon('cloud fw') = icon('cloud fw')
......
...@@ -24,17 +24,19 @@ ...@@ -24,17 +24,19 @@
.fade-left .fade-left
= nav_link(path: 'projects#show', html_options: {class: 'home'}) do = nav_link(path: 'projects#show', html_options: {class: 'home'}) do
= link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do = link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do
= icon('bookmark fw') = navbar_icon('project')
%span %span
Project Project
= nav_link(path: 'projects#activity') do = nav_link(path: 'projects#activity') do
= link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do = link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do
= icon('dashboard fw') = navbar_icon('activity')
%span %span
Activity Activity
- if project_nav_tab? :files - if project_nav_tab? :files
= nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare repositories tags branches releases network)) do = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare repositories tags branches releases network)) do
= link_to project_files_path(@project), title: 'Files', class: 'shortcuts-tree' do = link_to project_files_path(@project), title: 'Code', class: 'shortcuts-tree' do
= icon('code fw') = icon('code fw')
%span %span
Code Code
...@@ -42,7 +44,7 @@ ...@@ -42,7 +44,7 @@
- if project_nav_tab? :pipelines - if project_nav_tab? :pipelines
= nav_link(controller: :pipelines) do = nav_link(controller: :pipelines) do
= link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
= icon('ship fw') = navbar_icon('pipelines')
%span %span
Pipelines Pipelines
...@@ -63,14 +65,14 @@ ...@@ -63,14 +65,14 @@
- if project_nav_tab? :milestones - if project_nav_tab? :milestones
= nav_link(controller: :milestones) do = nav_link(controller: :milestones) do
= link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do
= icon('clock-o fw') = navbar_icon('milestones')
%span %span
Milestones Milestones
- if project_nav_tab? :issues - if project_nav_tab? :issues
= nav_link(controller: :issues) do = nav_link(controller: :issues) do
= link_to url_for_project_issues(@project, only_path: true), title: 'Issues', class: 'shortcuts-issues' do = link_to url_for_project_issues(@project, only_path: true), title: 'Issues', class: 'shortcuts-issues' do
= icon('exclamation-circle fw') = navbar_icon('issues')
%span %span
Issues Issues
- if @project.default_issues_tracker? - if @project.default_issues_tracker?
...@@ -79,7 +81,7 @@ ...@@ -79,7 +81,7 @@
- if project_nav_tab? :merge_requests - if project_nav_tab? :merge_requests
= nav_link(controller: :merge_requests) do = nav_link(controller: :merge_requests) do
= link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do
= icon('tasks fw') = navbar_icon('mr')
%span %span
Merge Requests Merge Requests
%span.badge.count.merge_counter= number_with_delimiter(@project.merge_requests.opened.count) %span.badge.count.merge_counter= number_with_delimiter(@project.merge_requests.opened.count)
...@@ -94,7 +96,7 @@ ...@@ -94,7 +96,7 @@
- if project_nav_tab? :wiki - if project_nav_tab? :wiki
= nav_link(controller: :wikis) do = nav_link(controller: :wikis) do
= link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do
= icon('book fw') = navbar_icon('wiki')
%span %span
Wiki Wiki
......
...@@ -5,8 +5,8 @@ ...@@ -5,8 +5,8 @@
- content_for :scripts_body_top do - content_for :scripts_body_top do
- project = @target_project || @project - project = @target_project || @project
- if @project_wiki - if @project_wiki && @page
- markdown_preview_path = namespace_project_wikis_markdown_preview_path(project.namespace, project) - markdown_preview_path = namespace_project_wiki_markdown_preview_path(project.namespace, project, params[:id])
- else - else
- markdown_preview_path = markdown_preview_namespace_project_path(project.namespace, project) - markdown_preview_path = markdown_preview_namespace_project_path(project.namespace, project)
- if current_user - if current_user
......
- empty_repo = @project.empty_repo? - empty_repo = @project.empty_repo?
.project-home-panel.cover-block.clearfix{:class => ("empty-project" if empty_repo)} .project-home-panel.cover-block.clearfix{:class => ("empty-project" if empty_repo)}
.container-fluid.container-limited %div{ class: (container_class) }
.row .row
.project-image-container .project-image-container
= project_icon(@project, alt: '', class: 'project-avatar avatar s70') = project_icon(@project, alt: '', class: 'project-avatar avatar s70')
......
...@@ -21,12 +21,10 @@ ...@@ -21,12 +21,10 @@
.controls.hidden-xs .controls.hidden-xs
- if create_mr_button?(@repository.root_ref, branch.name) - if create_mr_button?(@repository.root_ref, branch.name)
= link_to create_mr_path(@repository.root_ref, branch.name), class: 'btn btn-grouped btn-xs' do = link_to create_mr_path(@repository.root_ref, branch.name), class: 'btn btn-grouped btn-xs' do
= icon('plus')
Merge Request Merge Request
- if branch.name != @repository.root_ref - if branch.name != @repository.root_ref
= link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: 'btn btn-grouped btn-xs', method: :post, title: "Compare" do = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: 'btn btn-grouped btn-xs', method: :post, title: "Compare" do
= icon("exchange")
Compare Compare
- if can_remove_branch?(@project, branch.name) - if can_remove_branch?(@project, branch.name)
......
...@@ -3,13 +3,14 @@ ...@@ -3,13 +3,14 @@
= render "projects/commits/head" = render "projects/commits/head"
%div{ class: (container_class) } %div{ class: (container_class) }
.row-content-block.second-block.content-component-block .top-area
.pull-right .nav-text
Protected branches can be managed in project settings
- if can? current_user, :push_code, @project - if can? current_user, :push_code, @project
.nav-controls
= link_to new_namespace_project_branch_path(@project.namespace, @project), class: 'btn btn-create' do = link_to new_namespace_project_branch_path(@project.namespace, @project), class: 'btn btn-create' do
= icon('plus')
New branch New branch
&nbsp;
.dropdown.inline .dropdown.inline
%button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
%span.light %span.light
...@@ -26,8 +27,6 @@ ...@@ -26,8 +27,6 @@
= sort_title_recently_updated = sort_title_recently_updated
= link_to namespace_project_branches_path(sort: 'last_updated') do = link_to namespace_project_branches_path(sort: 'last_updated') do
= sort_title_oldest_updated = sort_title_oldest_updated
.oneline
Protected branches can be managed in project settings
- unless @branches.empty? - unless @branches.empty?
%ul.content-list.all-branches %ul.content-list.all-branches
- @branches.each do |branch| - @branches.each do |branch|
......
...@@ -34,7 +34,6 @@ ...@@ -34,7 +34,6 @@
= link_to 'Get started with Builds', help_page_path('ci/quick_start', 'README'), class: 'btn btn-info' = link_to 'Get started with Builds', help_page_path('ci/quick_start', 'README'), class: 'btn btn-info'
= link_to ci_lint_path, class: 'btn btn-default' do = link_to ci_lint_path, class: 'btn btn-default' do
= icon('wrench')
%span CI Lint %span CI Lint
%ul.content-list %ul.content-list
......
- page_title "Webhooks" = render 'shared/web_hooks/form', hook: @hook, hooks: @hooks, url_components: [@project.namespace.becomes(Namespace), @project]
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
%h4.prepend-top-0
= page_title
%p
#{link_to "Webhooks", help_page_path("web_hooks", "web_hooks")} can be
used for binding events when something is happening within the project.
.col-lg-9.append-bottom-default
%h5.prepend-top-0
Add new webhook
= form_for [@project.namespace.becomes(Namespace), @project, @hook], as: :hook, url: namespace_project_hooks_path(@project.namespace, @project) do |f|
= form_errors(@hook)
.form-group
= f.label :url, "URL", class: "label-light"
= f.text_field :url, class: "form-control", placeholder: "http://example.com/trigger-ci.json"
.form-group
= f.label :token, "Secret Token", class: 'label-light'
= f.text_field :token, class: "form-control", placeholder: ''
%p.help-block
Use this token to validate received payloads
.form-group
= f.label :url, "Trigger", class: "label-light"
%div
= f.check_box :push_events, class: "pull-left"
.prepend-left-20
= f.label :push_events, class: "label-light append-bottom-0" do
Push events
%p.light
This url will be triggered by a push to the repository
%div
= f.check_box :tag_push_events, class: "pull-left"
.prepend-left-20
= f.label :tag_push_events, class: "label-light append-bottom-0" do
Tag push events
%p.light
This url will be triggered when a new tag is pushed to the repository
%div
= f.check_box :note_events, class: "pull-left"
.prepend-left-20
= f.label :note_events, class: "label-light append-bottom-0" do
Comments
%p.light
This url will be triggered when someone adds a comment
%div
= f.check_box :issues_events, class: "pull-left"
.prepend-left-20
= f.label :issues_events, class: "label-light append-bottom-0" do
Issues events
%p.light
This url will be triggered when an issue is created/updated/merged
%div
= f.check_box :merge_requests_events, class: "pull-left"
.prepend-left-20
= f.label :merge_requests_events, class: "label-light append-bottom-0" do
Merge Request events
%p.light
This url will be triggered when a merge request is created/updated/merged
%div
= f.check_box :build_events, class: "pull-left"
.prepend-left-20
= f.label :build_events, class: "label-light append-bottom-0" do
Build events
%p.light
This url will be triggered when the build status changes
%div
= f.check_box :wiki_page_events, class: 'pull-left'
.prepend-left-20
= f.label :wiki_page_events, class: 'label-light append-bottom-0' do
Wiki Page events
%p.light
This url will be triggered when a wiki page is created/updated
.form-group
= f.label :enable_ssl_verification, "SSL verification", class: "label-light"
%div
= f.check_box :enable_ssl_verification, class: "pull-left"
.prepend-left-20
= f.label :enable_ssl_verification, class: "label-light append-bottom-0" do
Enable SSL verification
= f.submit "Add Webhook", class: "btn btn-create"
%hr
%h5.prepend-top-default
Webhooks (#{@hooks.count})
- if @hooks.any?
%ul.well-list
- @hooks.each do |hook|
= render "project_hook", hook: hook
- else
%p.settings-message.text-center.append-bottom-0
No webhooks found, add one in the form above.
- if can?(current_user, :push_code, @project) - if can?(current_user, :push_code, @project)
.pull-right .pull-right
#new-branch{'data-path' => can_create_branch_namespace_project_issue_path(@project.namespace, @project, @issue)} #new-branch{'data-path' => can_create_branch_namespace_project_issue_path(@project.namespace, @project, @issue)}
= link_to namespace_project_branches_path(@project.namespace, @project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid), method: :post, class: 'btn has-tooltip', title: @issue.to_branch_name, disabled: 'disabled' do = link_to namespace_project_branches_path(@project.namespace, @project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid),
method: :post, class: 'btn has-tooltip', title: @issue.to_branch_name, disabled: 'disabled' do
.checking .checking
%i.fa.fa-spinner.fa-spin = icon('spinner spin')
Checking branches Checking branches
.available(style="display: none") .available.hide
%i.fa.fa-code-fork
New branch New branch
.unavailable(style="display: none") .unavailable.hide
%i.fa.fa-exclamation-triangle = icon('exclamation-triangle')
New branch unavailable New branch unavailable
...@@ -15,7 +15,6 @@ ...@@ -15,7 +15,6 @@
= render 'shared/issuable/search_form', path: namespace_project_issues_path(@project.namespace, @project) = render 'shared/issuable/search_form', path: namespace_project_issues_path(@project.namespace, @project)
- if can? current_user, :create_issue, @project - if can? current_user, :create_issue, @project
= link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { assignee_id: @issuable_finder.assignee.try(:id), milestone_id: @issuable_finder.milestones.try(:first).try(:id) }), class: "btn btn-new", title: "New Issue", id: "new_issue_link" do = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { assignee_id: @issuable_finder.assignee.try(:id), milestone_id: @issuable_finder.milestones.try(:first).try(:id) }), class: "btn btn-new", title: "New Issue", id: "new_issue_link" do
= icon('plus')
New Issue New Issue
= render 'shared/issuable/filter', type: :issues = render 'shared/issuable/filter', type: :issues
......
...@@ -38,14 +38,12 @@ ...@@ -38,14 +38,12 @@
%li %li
= link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue) = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue)
- if can?(current_user, :create_issue, @project) - if can?(current_user, :create_issue, @project)
= link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-nr btn-grouped new-issue-link btn-success', title: 'New issue', id: 'new_issue_link' do = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-success', title: 'New issue', id: 'new_issue_link' do
= icon('plus')
New issue New issue
- if can?(current_user, :update_issue, @issue) - if can?(current_user, :update_issue, @issue)
= link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-nr btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-nr btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
= link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-nr btn-grouped issuable-edit' do = link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit' do
= icon('pencil-square-o')
Edit Edit
......
- label_css_id = dom_id(label) - label_css_id = dom_id(label)
%li{id: label_css_id, data: { id: label.id } } %li{id: label_css_id, data: { id: label.id } }
= render "shared/label_row", label: label = render "shared/label_row", label: label
.pull-info-right
%span.append-right-20 .visible-xs.visible-sm-inline-block.visible-md-inline-block.dropdown
%button.btn.btn-default.label-options-toggle{ data: { toggle: "dropdown" } }
Options
%span.caret
.dropdown-menu.dropdown-menu-align-right
%ul
%li
= link_to_label(label, type: :merge_request) do = link_to_label(label, type: :merge_request) do
= pluralize label.open_merge_requests_count, 'merge request' = pluralize label.open_merge_requests_count, 'merge request'
%li
%span.append-right-20
= link_to_label(label) do = link_to_label(label) do
= pluralize label.open_issues_count(current_user), 'open issue' = pluralize label.open_issues_count(current_user), 'open issue'
- if current_user - if current_user
.label-subscription{ data: { url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } } %li.label-subscription{ data: { url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } }
.subscription-status{ data: { status: label_subscription_status(label) } } %a.js-subscribe-button.label-subscribe-button.subscription-status{ role: "button", href: "#", data: { toggle: "tooltip", status: label_subscription_status(label) } }
%button.js-subscribe-button.label-subscribe-button.btn.action-buttons{ type: "button", data: { toggle: "tooltip" } }
%span= label_subscription_toggle_button_text(label) %span= label_subscription_toggle_button_text(label)
- if can? current_user, :admin_label, @project
%li
= link_to "Edit", edit_namespace_project_label_path(@project.namespace, @project, label)
%li
= link_to "Delete", namespace_project_label_path(@project.namespace, @project, label), title: "Delete", method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?"}
.pull-right.hidden-xs.hidden-sm.hidden-md
= link_to_label(label, type: :merge_request, css_class: 'btn btn-transparent btn-action') do
= pluralize label.open_merge_requests_count, 'merge request'
= link_to_label(label, css_class: 'btn btn-transparent btn-action') do
= pluralize label.open_issues_count(current_user), 'open issue'
- if can?(current_user, :admin_label, @project) - if current_user
= link_to edit_namespace_project_label_path(@project.namespace, @project, label), title: "Edit", class: 'btn action-buttons', data: { toggle: 'tooltip' } do .label-subscription.inline{ data: { url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } }
%i.fa.fa-pencil-square-o %button.js-subscribe-button.label-subscribe-button.btn.btn-transparent.btn-action.subscription-status{ type: "button", title: label_subscription_toggle_button_text(label), data: { toggle: "tooltip", status: label_subscription_status(label) } }
= link_to namespace_project_label_path(@project.namespace, @project, label), title: "Delete", class: 'btn action-buttons remove-row', method: :delete, remote: true, data: { confirm: 'Remove this label? Are you sure?', toggle: 'tooltip' } do %span.sr-only= label_subscription_toggle_button_text(label)
%i.fa.fa-trash-o = icon('eye', class: 'label-subscribe-button-icon')
= icon('spinner spin', class: 'label-subscribe-button-loading')
- if current_user - if can? current_user, :admin_label, @project
= link_to edit_namespace_project_label_path(@project.namespace, @project, label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do
%span.sr-only Edit
= icon('pencil-square-o')
= link_to namespace_project_label_path(@project.namespace, @project, label), title: "Delete", class: 'btn btn-transparent btn-action remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?", toggle: "tooltip"} do
%span.sr-only Delete
= icon('trash-o')
- if current_user
:javascript :javascript
new Subscription('##{label_css_id} .label-subscription'); new Subscription('##{dom_id(label)} .label-subscription');
...@@ -7,7 +7,6 @@ ...@@ -7,7 +7,6 @@
.nav-controls .nav-controls
- if can?(current_user, :admin_label, @project) - if can?(current_user, :admin_label, @project)
= link_to new_namespace_project_label_path(@project.namespace, @project), class: "btn btn-new" do = link_to new_namespace_project_label_path(@project.namespace, @project), class: "btn btn-new" do
= icon('plus')
New label New label
.labels .labels
......
...@@ -14,13 +14,11 @@ ...@@ -14,13 +14,11 @@
- if @merge_request.open? - if @merge_request.open?
.pull-right .pull-right
- if @merge_request.source_branch_exists? - if @merge_request.source_branch_exists?
= link_to "#modal_merge_info", class: "btn btn-sm", "data-toggle" => "modal" do = link_to "#modal_merge_info", class: "btn inline btn-grouped btn-sm", "data-toggle" => "modal" do
= icon('cloud-download fw')
Check out branch Check out branch
%span.dropdown %span.dropdown
%a.btn.btn-sm.dropdown-toggle{ data: {toggle: :dropdown} } %a.btn.btn-sm.dropdown-toggle{ data: {toggle: :dropdown} }
= icon('download')
Download as Download as
%span.caret %span.caret
%ul.dropdown-menu %ul.dropdown-menu
......
...@@ -10,7 +10,6 @@ ...@@ -10,7 +10,6 @@
- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
- if merge_project - if merge_project
= link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project), class: "btn btn-new", title: "New Merge Request" do = link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project), class: "btn btn-new", title: "New Merge Request" do
= icon('plus')
New Merge Request New Merge Request
= render 'shared/issuable/filter', type: :merge_requests = render 'shared/issuable/filter', type: :merge_requests
......
...@@ -25,8 +25,7 @@ ...@@ -25,8 +25,7 @@
= link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request' = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request'
%li %li
= link_to 'Edit', edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'issuable-edit' = link_to 'Edit', edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'issuable-edit'
= link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: "hidden-xs hidden-sm btn btn-nr btn-grouped btn-close #{issue_button_visibility(@merge_request, true)}", title: 'Close merge request' = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@merge_request, true)}", title: 'Close merge request'
= link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "hidden-xs hidden-sm btn btn-nr btn-grouped btn-reopen reopen-mr-link #{issue_button_visibility(@merge_request, false)}", title: 'Reopen merge request' = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen reopen-mr-link #{issue_button_visibility(@merge_request, false)}", title: 'Reopen merge request'
= link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "hidden-xs hidden-sm btn btn-nr btn-grouped issuable-edit" do = link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "hidden-xs hidden-sm btn btn-grouped issuable-edit" do
= icon('pencil-square-o')
Edit Edit
%h4 %h4.has-conflicts
= icon("exclamation-triangle") = icon("exclamation-triangle")
This merge request contains merge conflicts This merge request contains merge conflicts
......
...@@ -17,9 +17,8 @@ ...@@ -17,9 +17,8 @@
.col-md-6 .col-md-6
.form-group .form-group
= f.label :due_date, "Due Date", class: "control-label" = f.label :due_date, "Due Date", class: "control-label"
.col-sm-10= f.hidden_field :due_date
.col-sm-10 .col-sm-10
.datepicker = f.text_field :due_date, class: "datepicker form-control", placeholder: "Select due date"
.form-actions .form-actions
- if @milestone.new_record? - if @milestone.new_record?
......
...@@ -6,7 +6,6 @@ ...@@ -6,7 +6,6 @@
.nav-controls .nav-controls
- if can?(current_user, :admin_milestone, @project) - if can?(current_user, :admin_milestone, @project)
= link_to new_namespace_project_milestone_path(@project.namespace, @project), class: "btn btn-new", title: "New Milestone" do = link_to new_namespace_project_milestone_path(@project.namespace, @project), class: "btn btn-new", title: "New Milestone" do
= icon('plus')
New Milestone New Milestone
.milestones .milestones
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
- if @milestone.closed? - if @milestone.closed?
Closed Closed
- elsif @milestone.expired? - elsif @milestone.expired?
Expired Past due
- else - else
Open Open
%span.identifier %span.identifier
...@@ -23,11 +23,9 @@ ...@@ -23,11 +23,9 @@
= link_to 'Reopen Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-nr btn-grouped" = link_to 'Reopen Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-nr btn-grouped"
= link_to edit_namespace_project_milestone_path(@project.namespace, @project, @milestone), class: "btn btn-grouped btn-nr" do = link_to edit_namespace_project_milestone_path(@project.namespace, @project, @milestone), class: "btn btn-grouped btn-nr" do
= icon('pencil-square-o')
Edit Edit
= link_to namespace_project_milestone_path(@project.namespace, @project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-danger" do = link_to namespace_project_milestone_path(@project.namespace, @project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-danger" do
= icon('trash-o')
Delete Delete
.detail-page-description.milestone-detail .detail-page-description.milestone-detail
......
...@@ -20,10 +20,11 @@ ...@@ -20,10 +20,11 @@
- access = note.project.team.human_max_access(note.author.id) - access = note.project.team.human_max_access(note.author.id)
- if access - if access
%span.note-role.hidden-xs= access %span.note-role.hidden-xs= access
- if note_editable - if current_user
= link_to '#', title: 'Award Emoji', class: 'note-action-button note-emoji-button js-add-award js-note-emoji', data: { position: 'right' } do = link_to '#', title: 'Award Emoji', class: 'note-action-button note-emoji-button js-add-award js-note-emoji', data: { position: 'right' } do
= icon('spinner spin') = icon('spinner spin')
= icon('smile-o') = icon('smile-o')
- if note_editable
= link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
= icon('pencil') = icon('pencil')
= link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button hidden-xs js-note-delete danger' do = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button hidden-xs js-note-delete danger' do
......
...@@ -28,14 +28,12 @@ ...@@ -28,14 +28,12 @@
.nav-controls .nav-controls
- if can? current_user, :create_pipeline, @project - if can? current_user, :create_pipeline, @project
= link_to new_namespace_project_pipeline_path(@project.namespace, @project), class: 'btn btn-create' do = link_to new_namespace_project_pipeline_path(@project.namespace, @project), class: 'btn btn-create' do
= icon('plus')
New pipeline New pipeline
- unless @repository.gitlab_ci_yml - unless @repository.gitlab_ci_yml
= link_to 'Get started with Pipelines', help_page_path('ci/quick_start', 'README'), class: 'btn btn-info' = link_to 'Get started with Pipelines', help_page_path('ci/quick_start', 'README'), class: 'btn btn-info'
= link_to ci_lint_path, class: 'btn btn-default' do = link_to ci_lint_path, class: 'btn btn-default' do
= icon('wrench')
%span CI Lint %span CI Lint
%ul.content-list.pipelines %ul.content-list.pipelines
......
...@@ -7,7 +7,6 @@ ...@@ -7,7 +7,6 @@
- if can?(current_user, :admin_group_member, @group) - if can?(current_user, :admin_group_member, @group)
.controls .controls
= link_to group_group_members_path(@group), class: 'btn' do = link_to group_group_members_path(@group), class: 'btn' do
= icon('pencil-square-o')
Manage group members Manage group members
%ul.content-list %ul.content-list
- members.limit(20).each do |member| - members.limit(20).each do |member|
......
...@@ -32,9 +32,9 @@ ...@@ -32,9 +32,9 @@
.pull-right .pull-right
%strong= member.human_access %strong= member.human_access
- if can?(current_user, :update_project_member, member) - if can?(current_user, :update_project_member, member)
= button_tag class: "btn-xs btn js-toggle-button", = button_tag class: "btn-xs btn-grouped inline btn js-toggle-button",
title: 'Edit access level', type: 'button' do title: 'Edit access level', type: 'button' do
%i.fa.fa-pencil-square-o = icon('pencil')
- if can?(current_user, :destroy_project_member, member) - if can?(current_user, :destroy_project_member, member)
&nbsp; &nbsp;
...@@ -44,7 +44,7 @@ ...@@ -44,7 +44,7 @@
Leave Leave
- else - else
= link_to namespace_project_project_member_path(@project.namespace, @project, member), data: { confirm: remove_from_project_team_message(@project, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from team' do = link_to namespace_project_project_member_path(@project.namespace, @project, member), data: { confirm: remove_from_project_team_message(@project, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from team' do
%i.fa.fa-minus.fa-inverse = icon('trash')
.edit-member.hide.js-toggle-content .edit-member.hide.js-toggle-content
%br %br
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
= render "home_panel" = render "home_panel"
.project-stats.row-content-block.second-block .project-stats.row-content-block.second-block
.container-fluid.container-limited %div{ class: (container_class) }
%ul.nav %ul.nav
%li %li
= link_to project_files_path(@project) do = link_to project_files_path(@project) do
......
%span.btn-group.btn-grouped %span.btn-group
= link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), class: 'btn btn-default', rel: 'nofollow' do = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), class: 'btn btn-default', rel: 'nofollow' do
%i.fa.fa-download %span Source code
%span source code
%a.btn.btn-default.dropdown-toggle{ 'data-toggle' => 'dropdown' } %a.btn.btn-default.dropdown-toggle{ 'data-toggle' => 'dropdown' }
%span.caret %span.caret
%span.sr-only %span.sr-only
...@@ -9,9 +8,7 @@ ...@@ -9,9 +8,7 @@
%ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' } %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' }
%li %li
= link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do
%i.fa.fa-download
%span Download zip %span Download zip
%li %li
= link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do
%i.fa.fa-download
%span Download tar.gz %span Download tar.gz
...@@ -15,11 +15,11 @@ ...@@ -15,11 +15,11 @@
= render 'projects/tags/download', ref: tag.name, project: @project = render 'projects/tags/download', ref: tag.name, project: @project
- if can?(current_user, :push_code, @project) - if can?(current_user, :push_code, @project)
= link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn-grouped btn has-tooltip', title: "Edit release notes" do = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn has-tooltip', title: "Edit release notes" do
= icon("pencil") = icon("pencil")
- if can?(current_user, :admin_project, @project) - if can?(current_user, :admin_project, @project)
= link_to namespace_project_tag_path(@project.namespace, @project, tag.name), class: 'btn btn-grouped btn-xs btn-remove remove-row has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{tag.name}' tag cannot be undone. Are you sure?", container: 'body' }, remote: true do = link_to namespace_project_tag_path(@project.namespace, @project, tag.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{tag.name}' tag cannot be undone. Are you sure?", container: 'body' }, remote: true do
= icon("trash-o") = icon("trash-o")
- if commit - if commit
......
...@@ -3,14 +3,14 @@ ...@@ -3,14 +3,14 @@
= render "projects/commits/head" = render "projects/commits/head"
%div{ class: (container_class) } %div{ class: (container_class) }
.row-content-block.second-block.content-component-block .top-area
.nav-text
Tags give the ability to mark specific points in history as being important
- if can? current_user, :push_code, @project - if can? current_user, :push_code, @project
.pull-right .nav-controls
= link_to new_namespace_project_tag_path(@project.namespace, @project), class: 'btn btn-create new-tag-btn' do = link_to new_namespace_project_tag_path(@project.namespace, @project), class: 'btn btn-create new-tag-btn' do
= icon('plus')
New tag New tag
.oneline
Tags give the ability to mark specific points in history as being important
.tags .tags
- unless @tags.empty? - unless @tags.empty?
......
- if (@page && @page.persisted?) - if (@page && @page.persisted?)
= link_to namespace_project_wiki_history_path(@project.namespace, @project, @page), class: "btn btn-grouped" do = link_to namespace_project_wiki_history_path(@project.namespace, @project, @page), class: "btn" do
Page History Page History
- if can?(current_user, :create_wiki, @project) - if can?(current_user, :create_wiki, @project)
= link_to namespace_project_wiki_edit_path(@project.namespace, @project, @page), class: "btn btn-grouped" do = link_to namespace_project_wiki_edit_path(@project.namespace, @project, @page), class: "btn" do
%i.fa.fa-pencil-square-o
Edit Edit
- if can?(current_user, :admin_wiki, @project) - if can?(current_user, :admin_wiki, @project)
= link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-remove" do = link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-remove" do
= icon('trash')
Delete Delete
...@@ -13,7 +13,6 @@ ...@@ -13,7 +13,6 @@
.nav-controls .nav-controls
- if can?(current_user, :create_wiki, @project) - if can?(current_user, :create_wiki, @project)
= link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do
= icon('plus')
New Page New Page
= render 'projects/wikis/new' = render 'projects/wikis/new'
...@@ -8,5 +8,6 @@ ...@@ -8,5 +8,6 @@
= icon('star') = icon('star')
%span.label-name %span.label-name
= link_to_label(label, tooltip: false) = link_to_label(label, tooltip: false)
%span.prepend-left-10 - if label.description
%span.label-description
= markdown(label.description, pipeline: :single_line) = markdown(label.description, pipeline: :single_line)
- labels.each do |label| - labels.each do |label|
%span.label-row %span.label-row.btn-group{ role: "group", aria: { label: escape_once(label.name) }, style: "color: #{text_color_for_bg(label.color)}" }
= link_to_label(label, tooltip: false) = link_to namespace_project_label_path(@project.namespace, @project, label),
class: "btn btn-transparent has-tooltip",
style: "background-color: #{label.color};",
title: escape_once(label.description),
data: { container: "body" } do
= escape_once label.name
%button.btn.btn-transparent.label-remove.js-label-filter-remove{ type: "button", style: "background-color: #{label.color};", data: { label: label.title } }
= icon("times")
- if @projects.any? - if @projects.any?
.prepend-left-10.project-item-select-holder .project-item-select-holder
= project_select_tag :project_path, class: "project-item-select", data: { include_groups: local_assigns[:include_groups], order_by: 'last_activity_at' } = project_select_tag :project_path, class: "project-item-select", data: { include_groups: local_assigns[:include_groups], order_by: 'last_activity_at' }
%a.btn.btn-new.new-project-item-select-button %a.btn.btn-new.new-project-item-select-button
= icon('plus')
= local_assigns[:label] = local_assigns[:label]
%b.caret %b.caret
......
...@@ -6,10 +6,10 @@ ...@@ -6,10 +6,10 @@
- if group_member - if group_member
.controls.hidden-xs .controls.hidden-xs
- if can?(current_user, :admin_group, group) - if can?(current_user, :admin_group, group)
= link_to edit_group_path(group), class: "btn-sm btn btn-grouped" do = link_to edit_group_path(group), class: "btn" do
%i.fa.fa-cogs = icon('cogs')
= link_to leave_group_group_members_path(group), data: { confirm: leave_group_message(group.name) }, method: :delete, class: "btn-sm btn btn-grouped", title: 'Leave this group' do = link_to leave_group_group_members_path(group), data: { confirm: leave_group_message(group.name) }, method: :delete, class: "btn", title: 'Leave this group' do
= icon('sign-out') = icon('sign-out')
.stats .stats
......
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch -->
<title>path-1</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M5,0 C4.448,0 4,0.448 4,1 L4,3 L1,3 C0.448,3 0,3.448 0,4 L0,9 C0,9.552 0.448,10 1,10 L5,10 L5,8 L11,8 L11,10 L15,10 C15.552,10 16,9.552 16,9 L16,4 C16,3.448 15.552,3 15,3 L12,3 L12,1 C12,0.448 11.552,0 11,0 L5,0 L5,0 L5,0 Z M6,2.5 C6,2.224 6.224,2 6.5,2 L9.5,2 C9.776,2 10,2.224 10,2.5 C10,2.776 9.776,3 9.5,3 L6.5,3 C6.224,3 6,2.776 6,2.5 L6,2.5 L6,2.5 Z M6,11 L10.001,11 L10.001,9 L6,9 L6,11 L6,11 L6,11 Z M11,11 L11,12 L5,12 L5,11 L1,11 C0.448,11 0,11.448 0,12 L0,15 C0,15.552 0.448,16 1,16 L15,16 C15.552,16 16,15.552 16,15 L16,12 C16,11.448 15.552,11 15,11 L11,11 L11,11 L11,11 Z" id="path-1"></path>
</defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="path-1" fill="#D8D8D8" xlink:href="#path-1"></use>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch -->
<title>Pasted Image 240</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M3,8 C3,5.951 4.236,4.194 6,3.422 L6,0 L1,0 C0.448,0 0,0.448 0,1 L0,15 C0,15.552 0.448,16 1,16 L6,16 L6,12.578 C4.236,11.806 3,10.049 3,8 M7,12.899 L7,16 L9,16 L9,12.899 C8.677,12.965 8.343,13 8,13 C7.657,13 7.323,12.965 7,12.899 M15,0 L10,0 L10,3.422 C11.764,4.194 13,5.951 13,8 C13,10.049 11.764,11.806 10,12.578 L10,16 L15,16 C15.552,16 16,15.552 16,15 L16,1 C16,0.448 15.552,0 15,0 M10,8 C10,9.105 9.105,10 8,10 C6.895,10 6,9.105 6,8 C6,6.895 6.895,6 8,6 C9.105,6 10,6.895 10,8 M4,8 C4,10.209 5.791,12 8,12 C10.209,12 12,10.209 12,8 C12,5.791 10.209,4 8,4 C5.791,4 4,5.791 4,8 M9,3.101 L9,0 L7,0 L7,3.101 C7.323,3.035 7.657,3 8,3 C8.343,3 8.677,3.035 9,3.101" id="Pasted-Image-240" fill="#7E7D7D"></path>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch -->
<title>Group</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group">
<path d="M8,0 C3.581,0 0,3.581 0,8 C0,12.419 3.581,16 8,16 C12.419,16 16,12.419 16,8 C16,3.581 12.419,0 8,0 M8,2 C11.308,2 14,4.692 14,8 C14,11.308 11.308,14 8,14 C4.692,14 2,11.308 2,8 C2,4.692 4.692,2 8,2" id="Fill-1" fill="#7E7C7C"></path>
<polygon id="Stroke-6" fill="#7E7C7C" points="2.0197351 9.86809696 6.4567351 6.52409696 5.79233671 6.46815759 9.53233671 10.4271576 9.87070552 10.78534 10.2338016 10.4522494 15.0258016 6.05624938 14.3497984 5.31935062 9.55779844 9.71535062 10.2592633 9.74044241 6.51926329 5.78144241 6.21208651 5.45627854 5.8548649 5.72550304 1.4178649 9.06950304"></polygon>
<path d="M7.0313,6.3928 C7.0313,6.9448 6.5833,7.3928 6.0313,7.3928 C5.4793,7.3928 5.0313,6.9448 5.0313,6.3928 C5.0313,5.8408 5.4793,5.3928 6.0313,5.3928 C6.5833,5.3928 7.0313,5.8408 7.0313,6.3928" id="Fill-8" fill="#FEFEFE"></path>
<path d="M6.5313,6.3928 C6.5313,6.66865763 6.30715763,6.8928 6.0313,6.8928 C5.75544237,6.8928 5.5313,6.66865763 5.5313,6.3928 C5.5313,6.11694237 5.75544237,5.8928 6.0313,5.8928 C6.30715763,5.8928 6.5313,6.11694237 6.5313,6.3928 L6.5313,6.3928 Z M7.5313,6.3928 C7.5313,5.56465763 6.85944237,4.8928 6.0313,4.8928 C5.20315763,4.8928 4.5313,5.56465763 4.5313,6.3928 C4.5313,7.22094237 5.20315763,7.8928 6.0313,7.8928 C6.85944237,7.8928 7.5313,7.22094237 7.5313,6.3928 L7.5313,6.3928 Z" id="Stroke-10" fill="#7E7C7C"></path>
<path d="M10.8854,9.8715 C10.8854,10.4235 10.4374,10.8715 9.8854,10.8715 C9.3334,10.8715 8.8854,10.4235 8.8854,9.8715 C8.8854,9.3195 9.3334,8.8715 9.8854,8.8715 C10.4374,8.8715 10.8854,9.3195 10.8854,9.8715" id="Fill-12" fill="#FEFEFE"></path>
<path d="M10.3854,9.8715 C10.3854,10.1473576 10.1612576,10.3715 9.8854,10.3715 C9.60954237,10.3715 9.3854,10.1473576 9.3854,9.8715 C9.3854,9.59564237 9.60954237,9.3715 9.8854,9.3715 C10.1612576,9.3715 10.3854,9.59564237 10.3854,9.8715 L10.3854,9.8715 Z M11.3854,9.8715 C11.3854,9.04335763 10.7135424,8.3715 9.8854,8.3715 C9.05725763,8.3715 8.3854,9.04335763 8.3854,9.8715 C8.3854,10.6996424 9.05725763,11.3715 9.8854,11.3715 C10.7135424,11.3715 11.3854,10.6996424 11.3854,9.8715 L11.3854,9.8715 Z" id="Stroke-14" fill="#7E7C7C"></path>
</g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch -->
<title>Pasted Image 237</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Pasted-Image-237">
<path d="M15.1111,16 C15.6021,16 16.0001,15.602 16.0001,15.111 L16.0001,4.444 C15.5341,3.983 12.0671,0.378 11.5551,0 L0.8891,0 C0.3981,0 0.0001,0.398 0.0001,0.889 L0.0001,15.111 C0.0001,15.602 0.3981,16 0.8891,16 L15.1111,16 M14.0001,14.111 L1.8891,14.111 L1.8891,2 L10.8131,2 C11.4451,2.42 13.5811,4.555 14.0001,5.187 L14.0001,14.111" id="Fill-1" fill="#7E7D7D"></path>
<path d="M0.889,0 C0.398,0 0,0.398 0,0.889 L0,15.111 C0,15.602 0.398,16 0.889,16 L15.111,16 C15.602,16 16,15.602 16,15.111 L16,4.445 C15.534,3.983 12.068,0.377 11.555,0 L0.889,0 L0.889,0 Z M1.889,2 L10.813,2 C11.446,2.42 13.581,4.554 14,5.187 L14,14.111 L1.889,14.111 L1.889,2 L1.889,2 Z" id="Clip-4"></path>
<polygon id="Fill-6" fill="#7E7D7D" points="9 7 11 7 11 2 9 2"></polygon>
<polygon id="Clip-9" points="9 7 11 7 11 2.001 9 2.001"></polygon>
<polygon id="Fill-11" fill="#7E7D7D" points="10 7 15.444 7 15.444 5 10 5"></polygon>
<polygon id="Clip-14" points="10 7 15.444 7 15.444 5 10 5"></polygon>
</g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch -->
<title>Group</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group" fill="#303030">
<path d="M15.6667,10.0105 L10.3337,10.0105 C10.1497,10.0105 9.9997,10.1775 9.9997,10.3845 L9.9997,15.6145 C9.9997,15.8215 10.1497,15.9885 10.3337,15.9885 L15.6667,15.9885 C15.8507,15.9885 15.9997,15.8215 15.9997,15.6145 L15.9997,10.3845 C15.9997,10.1775 15.8507,10.0105 15.6667,10.0105 L15.6667,10.0105 L15.6667,10.0105 Z M11.9997,14.0105 L13.9997,14.0105 L13.9997,12.0105 L11.9997,12.0105 L11.9997,14.0105 L11.9997,14.0105 Z" id="Fill-11"></path>
<path d="M5.6667,10.0105 L0.3337,10.0105 C0.1497,10.0105 -0.0003,10.1775 -0.0003,10.3845 L-0.0003,15.6145 C-0.0003,15.8215 0.1497,15.9885 0.3337,15.9885 L5.6667,15.9885 C5.8507,15.9885 5.9997,15.8215 5.9997,15.6145 L5.9997,10.3845 C5.9997,10.1775 5.8507,10.0105 5.6667,10.0105 L5.6667,10.0105 L5.6667,10.0105 Z M1.9997,14.0105 L3.9997,14.0105 L3.9997,12.0105 L1.9997,12.0105 L1.9997,14.0105 L1.9997,14.0105 Z" id="Fill-8"></path>
<polygon id="Stroke-1" points="12.5 7.5834 3.5 7.5834 3.5 9.5834 12.5 9.5834"></polygon>
<polygon id="Stroke-3" points="9 9.0834 9 5.0834 7 5.0834 7 9.0834"></polygon>
<polygon id="Stroke-4" points="4 11.0834 4 7.5834 2 7.5834 2 11.0834"></polygon>
<polygon id="Stroke-6" points="14 11.0834 14 7.5834 12 7.5834 12 11.0834"></polygon>
<path d="M11.6667,6.21724894e-15 L4.3337,6.21724894e-15 C4.1497,6.21724894e-15 3.9997,0.167 3.9997,0.374 L3.9997,6.604 C3.9997,6.811 4.1497,6.978 4.3337,6.978 L11.6667,6.978 C11.8507,6.978 11.9997,6.811 11.9997,6.604 L11.9997,0.374 C11.9997,0.167 11.8507,6.21724894e-15 11.6667,6.21724894e-15 L11.6667,6.21724894e-15 L11.6667,6.21724894e-15 Z M5.9997,5 L9.9997,5 L9.9997,2 L5.9997,2 L5.9997,5 L5.9997,5 Z" id="Fill-14"></path>
</g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch -->
<title>Group</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group" fill="#7E7C7C">
<path d="M8,0 C3.581,0 0,3.581 0,8 C0,12.419 3.581,16 8,16 C12.419,16 16,12.419 16,8 C16,3.581 12.419,0 8,0 M8,2 C11.308,2 14,4.692 14,8 C14,11.308 11.308,14 8,14 C4.692,14 2,11.308 2,8 C2,4.692 4.692,2 8,2" id="Fill-1"></path>
<path d="M7.1597,4 L8.8887,4 L8.8887,8 L7.1107,8 L7.1597,4 Z M7.1597,9.6667 L8.8887,9.6667 L8.8887,11.4447 L7.1107,11.4447 L7.1597,9.6667 Z" id="Combined-Shape"></path>
</g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="22px" height="16px" viewBox="0 0 22 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch -->
<title>Group</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group" fill="#7E7C7C">
<path d="M6.4357,11.8588 C7.1487,11.2798 7.8797,10.7808 8.5357,10.3708 C8.5837,10.3008 8.6187,10.2338 8.6187,10.1768 L8.6187,8.8088 C8.9197,8.5218 9.0927,8.1248 9.0927,7.7028 L9.0927,5.3748 C9.0927,3.9478 7.9187,2.7858 6.4757,2.7858 L5.9687,2.7858 C4.5247,2.7858 3.3507,3.9478 3.3507,5.3748 L3.3507,7.7028 C3.3507,8.1248 3.5247,8.5218 3.8247,8.8088 L3.8247,10.5838 C3.2537,10.8738 1.8797,11.6198 0.5967,12.6618 C0.2177,12.9698 -0.0003,13.4258 -0.0003,13.9138 L-0.0003,15.5088 C-0.0003,15.5438 0.0857,15.7668 0.3467,15.7778 C1.3257,15.8198 3.8417,15.8328 5.9617,15.9038 C5.8337,15.8148 5.7447,15.6748 5.7447,15.5088 L5.7447,13.5498 C5.7447,12.9848 5.9967,12.2158 6.4357,11.8588" id="Fill-1"></path>
<path d="M21.3092,12.1 C19.6932,10.787 17.9592,9.86 17.3042,9.53 L17.3042,7.235 C17.6722,6.9 17.8862,6.428 17.8862,5.925 L17.8862,3.066 C17.8862,1.376 16.4952,0 14.7852,0 L14.1632,0 C12.4532,0 11.0622,1.376 11.0622,3.066 L11.0622,5.925 C11.0622,6.428 11.2752,6.9 11.6442,7.235 L11.6442,9.53 C10.9892,9.86 9.2542,10.787 7.6392,12.1 C7.2002,12.457 6.9482,12.985 6.9482,13.55 L6.9482,15.509 C6.9482,15.78 7.1702,16 7.4442,16 L14.1172,16 L14.1172,11.704 C12.6812,11.595 11.5652,10.853 11.5652,9.945 C11.5652,9.804 11.5982,9.669 11.6482,9.538 C11.9502,10.326 13.0982,10.913 14.4762,10.913 C15.8532,10.913 17.0012,10.326 17.3032,9.538 C17.3532,9.669 17.3862,9.804 17.3862,9.945 C17.3862,10.793 16.4152,11.5 15.1172,11.679 L15.1172,16 L21.5032,16 C21.7772,16 22.0002,15.78 22.0002,15.509 L22.0002,13.55 C22.0002,12.985 21.7482,12.457 21.3092,12.1" id="Fill-4"></path>
</g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="16px" height="17px" viewBox="0 0 16 17" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch -->
<title>Group</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group" fill="#7E7C7C">
<path d="M15.1111,1 L0.8891,1 C0.3981,1 0.0001,1.446 0.0001,1.996 L0.0001,15.945 C0.0001,16.495 0.3981,16.941 0.8891,16.941 L15.1111,16.941 C15.6021,16.941 16.0001,16.495 16.0001,15.945 L16.0001,1.996 C16.0001,1.446 15.6021,1 15.1111,1 L15.1111,1 L15.1111,1 Z M14.0001,6.0002 L14.0001,14.949 L2.0001,14.949 L2.0001,6.0002 L14.0001,6.0002 Z M14.0001,4.0002 L14.0001,2.993 L2.0001,2.993 L2.0001,4.0002 L14.0001,4.0002 Z" id="Combined-Shape"></path>
<polygon id="Fill-11" points="3 2.0002 5 2.0002 5 0.0002 3 0.0002"></polygon>
<polygon id="Fill-16" points="11 2.0002 13 2.0002 13 0.0002 11 0.0002"></polygon>
<path d="M5.37709616,11.5511984 L6.92309616,12.7821984 C7.35112915,13.123019 7.97359761,13.0565604 8.32002627,12.6330535 L10.7740263,9.63305349 C11.1237073,9.20557058 11.0606364,8.57555475 10.6331535,8.22587373 C10.2056706,7.87619272 9.57565475,7.93926361 9.22597373,8.36674651 L6.77197373,11.3667465 L8.16890384,11.2176016 L6.62290384,9.98660159 C6.19085236,9.6425813 5.56172188,9.71394467 5.21770159,10.1459962 C4.8736813,10.5780476 4.94504467,11.2071781 5.37709616,11.5511984 L5.37709616,11.5511984 Z" id="Stroke-21"></path>
</g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch -->
<title>Group</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group" fill="#7E7C7C">
<path d="M15.1111,0 L0.8891,0 C0.3981,0 0.0001,0.446 0.0001,0.996 L0.0001,14.945 C0.0001,15.495 0.3981,15.941 0.8891,15.941 L15.1111,15.941 C15.6021,15.941 16.0001,15.495 16.0001,14.945 L16.0001,0.996 C16.0001,0.446 15.6021,0 15.1111,0 L15.1111,0 L15.1111,0 Z M2.0001,13.949 L14.0001,13.949 L14.0001,1.993 L2.0001,1.993 L2.0001,13.949 Z M2,5.0002 L14,5.0002 L14,3.0002 L2,3.0002 L2,5.0002 Z" id="Combined-Shape"></path>
<path d="M8.547,12.0002 L12,12.0002 L12,10.0002 L8.547,10.0002 L8.547,12.0002 Z M5.2029,12 L3.9999,10.867 L5.2029,9.501 L3.9999,8.181 L5.2029,7 L7.4529,9.499 L5.2029,12 Z" id="Combined-Shape"></path>
</g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch -->
<title>Pasted Image 246</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M12.5,14 C11.672,14 11,13.328 11,12.5 C11,11.672 11.672,11 12.5,11 C13.328,11 14,11.672 14,12.5 C14,13.328 13.328,14 12.5,14 M12.5,9 L3.5,9 C1.567,9 0,10.567 0,12.5 C0,14.433 1.567,16 3.5,16 L12.5,16 C14.433,16 16,14.433 16,12.5 C16,10.567 14.433,9 12.5,9 M3.5,2 C4.328,2 5,2.672 5,3.5 C5,4.328 4.328,5 3.5,5 C2.672,5 2,4.328 2,3.5 C2,2.672 2.672,2 3.5,2 M3.5,7 L12.5,7 C14.433,7 16,5.433 16,3.5 C16,1.567 14.433,0 12.5,0 L3.5,0 C1.567,0 0,1.567 0,3.5 C0,5.433 1.567,7 3.5,7" id="Pasted-Image-246" fill="#303030"></path>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch -->
<title>Page 1</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M6,6 L12,6 L12,5 L6,5 L6,6 Z M6,8 L12,8 L12,7 L6,7 L6,8 Z M6,10 L12,10 L12,9 L6,9 L6,10 Z M6,12 L12,12 L12,11 L6,11 L6,12 Z M4,6 L5,6 L5,5 L4,5 L4,6 Z M4,8 L5,8 L5,7 L4,7 L4,8 Z M4,10 L5,10 L5,9 L4,9 L4,10 Z M4,12 L5,12 L5,11 L4,11 L4,12 Z M13,3 L10,3 L10,4 L6,4 L6,3 L3,3 L3,13 L13,13 L13,3 Z M2,14 L14,14 L14,2 L2,2 L2,14 Z M1,0 C0.448,0 0,0.448 0,1 L0,15 C0,15.552 0.448,16 1,16 L15,16 C15.552,16 16,15.552 16,15 L16,1 C16,0.448 15.552,0 15,0 L1,0 Z" fill="#7F7E7E"></path>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch -->
<title>Pasted Image 241</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M2.004,12.9999459 L3.939,12.9999459 L3.939,4.99994585 L2.004,4.99994585 L2.004,12.9999459 Z M7.017,9.99994585 L13.018,9.99994585 L13.018,8.99994585 L7.017,8.99994585 L7.017,9.99994585 Z M7.017,7.99994585 L13.018,7.99994585 L13.018,6.99994585 L7.017,6.99994585 L7.017,7.99994585 Z M7.017,5.99994585 L13.018,5.99994585 L13.018,4.99994585 L7.017,4.99994585 L7.017,5.99994585 Z M14.754,-5.41499267e-05 L4.938,-5.41499267e-05 C4.386,-5.41499267e-05 3.938,0.44794585 3.938,0.99994585 L3.938,2.99994585 L1,2.99994585 C0.448,2.99994585 0,3.44794585 0,3.99994585 L0,12.9999459 C0.037,13.4999459 -0.25,16.0509459 3.938,15.9999459 L12.408,15.9999459 C12.408,15.9999459 15.754,15.9169459 15.754,13.9999459 L15.754,0.99994585 C15.754,0.44794585 15.306,-5.41499267e-05 14.754,-5.41499267e-05 L14.754,-5.41499267e-05 Z" id="Pasted-Image-241" fill="#7E7D7D"></path>
</g>
</svg>
\ No newline at end of file
...@@ -88,9 +88,9 @@ ...@@ -88,9 +88,9 @@
.col-lg-6 .col-lg-6
.form-group .form-group
= f.label :due_date, "Due date", class: "control-label" = f.label :due_date, "Due date", class: "control-label"
= f.hidden_field :due_date, id: "issuable-due-date"
.col-sm-10 .col-sm-10
.datepicker .issuable-form-select-holder
= f.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: "Select due date"
- if issuable.can_move?(current_user) - if issuable.can_move?(current_user)
%hr %hr
......
...@@ -41,6 +41,7 @@ ...@@ -41,6 +41,7 @@
= icon('clock-o') = icon('clock-o')
%span %span
- if issuable.milestone - if issuable.milestone
%span.has-tooltip{title: milestone_remaining_days(issuable.milestone), data: {container: 'body', html: 1, placement: 'left'}}
= issuable.milestone.title = issuable.milestone.title
- else - else
None None
...@@ -52,6 +53,7 @@ ...@@ -52,6 +53,7 @@
.value.bold.hide-collapsed .value.bold.hide-collapsed
- if issuable.milestone - if issuable.milestone
= link_to namespace_project_milestone_path(@project.namespace, @project, issuable.milestone) do = link_to namespace_project_milestone_path(@project.namespace, @project, issuable.milestone) do
%span.has-tooltip{title: milestone_remaining_days(issuable.milestone), data: {container: 'body', html: 1}}
= issuable.milestone.title = issuable.milestone.title
- else - else
.light None .light None
...@@ -133,7 +135,7 @@ ...@@ -133,7 +135,7 @@
.title.hide-collapsed .title.hide-collapsed
Notifications Notifications
- subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed' - subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed'
%button.btn.btn-block.btn-gray.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" } %button.btn.btn-block.btn-default.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" }
%span= subscribed ? 'Unsubscribe' : 'Subscribe' %span= subscribed ? 'Unsubscribe' : 'Subscribe'
.subscription-status.hide-collapsed{data: {status: subscribtion_status}} .subscription-status.hide-collapsed{data: {status: subscribtion_status}}
.unsubscribed{class: ( 'hidden' if subscribed )} .unsubscribed{class: ( 'hidden' if subscribed )}
......
...@@ -35,11 +35,9 @@ ...@@ -35,11 +35,9 @@
.col-sm-6= render('shared/milestone_expired', milestone: milestone) .col-sm-6= render('shared/milestone_expired', milestone: milestone)
.col-sm-6 .col-sm-6
- if can?(current_user, :admin_milestone, milestone.project) and milestone.active? - if can?(current_user, :admin_milestone, milestone.project) and milestone.active?
= link_to edit_namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), class: "btn btn-xs" do = link_to edit_namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), class: "btn btn-xs btn-grouped" do
= icon('pencil-square-o')
Edit Edit
\ \
= link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-xs btn-close" = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-xs btn-close btn-grouped"
= link_to namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-xs btn-remove" do = link_to namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-xs btn-remove btn-grouped" do
= icon('trash-o')
Delete Delete
- page_title "Webhooks"
- context_title = @project ? 'project' : 'group'
.row.prepend-top-default
.col-lg-3
%h4.prepend-top-0
= page_title
%p
#{link_to "Webhooks", help_page_path("web_hooks", "web_hooks")} can be
used for binding events when something is happening within the project.
.col-lg-9.append-bottom-default
= form_for hook, as: :hook, url: polymorphic_path(url_components + [:hooks]) do |f|
= form_errors(hook)
.form-group
= f.label :url, "URL", class: 'label-light'
= f.text_field :url, class: "form-control", placeholder: 'http://example.com/trigger-ci.json'
.form-group
= f.label :token, "Secret Token", class: 'label-light'
= f.text_field :token, class: "form-control", placeholder: ''
%p.help-block
Use this token to validate received payloads
.form-group
= f.label :url, "Trigger", class: 'label-light'
%ul.list-unstyled
%li
= f.check_box :push_events, class: 'pull-left'
.prepend-left-20
= f.label :push_events, class: 'list-label' do
%strong Push events
%p.light
This url will be triggered by a push to the repository
%li
= f.check_box :tag_push_events, class: 'pull-left'
.prepend-left-20
= f.label :tag_push_events, class: 'list-label' do
%strong Tag push events
%p.light
This url will be triggered when a new tag is pushed to the repository
%li
= f.check_box :note_events, class: 'pull-left'
.prepend-left-20
= f.label :note_events, class: 'list-label' do
%strong Comments
%p.light
This url will be triggered when someone adds a comment
%li
= f.check_box :issues_events, class: 'pull-left'
.prepend-left-20
= f.label :issues_events, class: 'list-label' do
%strong Issues events
%p.light
This url will be triggered when an issue is created/updated/merged
%li
= f.check_box :merge_requests_events, class: 'pull-left'
.prepend-left-20
= f.label :merge_requests_events, class: 'list-label' do
%strong Merge Request events
%p.light
This url will be triggered when a merge request is created/updated/merged
%li
= f.check_box :build_events, class: 'pull-left'
.prepend-left-20
= f.label :build_events, class: 'list-label' do
%strong Build events
%p.light
This url will be triggered when the build status changes
%li
= f.check_box :wiki_page_events, class: 'pull-left'
.prepend-left-20
= f.label :wiki_page_events, class: 'list-label' do
%strong Wiki Page events
%p.light
This url will be triggered when a wiki page is created/updated
.form-group
= f.label :enable_ssl_verification, "SSL verification", class: 'label-light checkbox'
.checkbox
= f.label :enable_ssl_verification do
= f.check_box :enable_ssl_verification
%strong Enable SSL verification
= f.submit "Add Webhook", class: "btn btn-create"
%hr
%h5.prepend-top-default
Webhooks (#{hooks.count})
- if hooks.any?
%ul.well-list
- hooks.each do |hook|
= render "project_hook", hook: hook
- else
%p.settings-message.text-center.append-bottom-0
No webhooks found, add one in the form above.
...@@ -79,10 +79,10 @@ ...@@ -79,10 +79,10 @@
%li.js-contributed-tab %li.js-contributed-tab
= link_to user_contributed_projects_path, data: {target: 'div#contributed', action: 'contributed', toggle: 'tab'} do = link_to user_contributed_projects_path, data: {target: 'div#contributed', action: 'contributed', toggle: 'tab'} do
Contributed projects Contributed projects
%li.projects-tab %li.js-projects-tab
= link_to user_projects_path, data: {target: 'div#projects', action: 'projects', toggle: 'tab'} do = link_to user_projects_path, data: {target: 'div#projects', action: 'projects', toggle: 'tab'} do
Personal projects Personal projects
%li.snippets-tab %li.js-snippets-tab
= link_to user_snippets_path, data: {target: 'div#snippets', action: 'snippets', toggle: 'tab'} do = link_to user_snippets_path, data: {target: 'div#snippets', action: 'snippets', toggle: 'tab'} do
Snippets Snippets
......
...@@ -12,7 +12,7 @@ Doorkeeper.configure do ...@@ -12,7 +12,7 @@ Doorkeeper.configure do
end end
resource_owner_from_credentials do |routes| resource_owner_from_credentials do |routes|
Gitlab::Auth.new.find(params[:username], params[:password]) Gitlab::Auth.find_in_gitlab_or_ldap(params[:username], params[:password])
end end
# If you want to restrict access to the web interface for adding oauth authorized applications, you need to declare the block below. # If you want to restrict access to the web interface for adding oauth authorized applications, you need to declare the block below.
......
...@@ -80,8 +80,8 @@ Rails.application.routes.draw do ...@@ -80,8 +80,8 @@ Rails.application.routes.draw do
# Health check # Health check
get 'health_check(/:checks)' => 'health_check#index', as: :health_check get 'health_check(/:checks)' => 'health_check#index', as: :health_check
# Enable Grack support # Enable Grack support (for LFS only)
mount Grack::AuthSpawner, at: '/', constraints: lambda { |request| /[-\/\w\.]+\.git\//.match(request.path_info) }, via: [:get, :post, :put] mount Grack::AuthSpawner, at: '/', constraints: lambda { |request| /[-\/\w\.]+\.git\/(info\/lfs|gitlab-lfs)/.match(request.path_info) }, via: [:get, :post, :put]
# Help # Help
get 'help' => 'help#index' get 'help' => 'help#index'
...@@ -441,6 +441,7 @@ Rails.application.routes.draw do ...@@ -441,6 +441,7 @@ Rails.application.routes.draw do
resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do
resources(:projects, constraints: { id: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }, except: resources(:projects, constraints: { id: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }, except:
[:new, :create, :index], path: "/") do [:new, :create, :index], path: "/") do
member do member do
put :transfer put :transfer
delete :remove_fork delete :remove_fork
...@@ -454,6 +455,29 @@ Rails.application.routes.draw do ...@@ -454,6 +455,29 @@ Rails.application.routes.draw do
end end
scope module: :projects do scope module: :projects do
# Git HTTP clients ('git clone' etc.)
scope constraints: { id: /.+\.git/, format: nil } do
get '/info/refs', to: 'git_http#info_refs'
post '/git-upload-pack', to: 'git_http#git_upload_pack'
post '/git-receive-pack', to: 'git_http#git_receive_pack'
end
# Allow /info/refs, /info/refs?service=git-upload-pack, and
# /info/refs?service=git-receive-pack, but nothing else.
#
git_http_handshake = lambda do |request|
request.query_string.blank? ||
request.query_string.match(/\Aservice=git-(upload|receive)-pack\z/)
end
ref_redirect = redirect do |params, request|
path = "#{params[:namespace_id]}/#{params[:project_id]}.git/info/refs"
path << "?#{request.query_string}" unless request.query_string.blank?
path
end
get '/info/refs', constraints: git_http_handshake, to: ref_redirect
# Blob routes: # Blob routes:
get '/new/*id', to: 'blob#new', constraints: { id: /.+/ }, as: 'new_blob' get '/new/*id', to: 'blob#new', constraints: { id: /.+/ }, as: 'new_blob'
post '/create/*id', to: 'blob#create', constraints: { id: /.+/ }, as: 'create_blob' post '/create/*id', to: 'blob#create', constraints: { id: /.+/ }, as: 'create_blob'
...@@ -592,7 +616,6 @@ Rails.application.routes.draw do ...@@ -592,7 +616,6 @@ Rails.application.routes.draw do
# Order matters to give priority to these matches # Order matters to give priority to these matches
get '/wikis/git_access', to: 'wikis#git_access' get '/wikis/git_access', to: 'wikis#git_access'
get '/wikis/pages', to: 'wikis#pages', as: 'wiki_pages' get '/wikis/pages', to: 'wikis#pages', as: 'wiki_pages'
post '/wikis/markdown_preview', to:'wikis#markdown_preview'
post '/wikis', to: 'wikis#create' post '/wikis', to: 'wikis#create'
get '/wikis/*id/history', to: 'wikis#history', as: 'wiki_history', constraints: WIKI_SLUG_ID get '/wikis/*id/history', to: 'wikis#history', as: 'wiki_history', constraints: WIKI_SLUG_ID
...@@ -601,6 +624,7 @@ Rails.application.routes.draw do ...@@ -601,6 +624,7 @@ Rails.application.routes.draw do
get '/wikis/*id', to: 'wikis#show', as: 'wiki', constraints: WIKI_SLUG_ID get '/wikis/*id', to: 'wikis#show', as: 'wiki', constraints: WIKI_SLUG_ID
delete '/wikis/*id', to: 'wikis#destroy', constraints: WIKI_SLUG_ID delete '/wikis/*id', to: 'wikis#destroy', constraints: WIKI_SLUG_ID
put '/wikis/*id', to: 'wikis#update', constraints: WIKI_SLUG_ID put '/wikis/*id', to: 'wikis#update', constraints: WIKI_SLUG_ID
post '/wikis/*id/markdown_preview', to:'wikis#markdown_preview', constraints: WIKI_SLUG_ID, as: 'wiki_markdown_preview'
end end
resource :repository, only: [:show, :create] do resource :repository, only: [:show, :create] do
......
class RemoveDuplicatedNotificationSettings < ActiveRecord::Migration class RemoveDuplicatedNotificationSettings < ActiveRecord::Migration
def up def up
execute <<-SQL duplicates = exec_query(%Q{
DELETE FROM notification_settings WHERE id NOT IN ( SELECT min_id from (SELECT MIN(id) as min_id FROM notification_settings GROUP BY user_id, source_type, source_id) as dups ) SELECT user_id, source_type, source_id
SQL FROM notification_settings
GROUP BY user_id, source_type, source_id
HAVING COUNT(*) > 1
})
duplicates.each do |row|
uid = row['user_id']
stype = connection.quote(row['source_type'])
sid = row['source_id']
execute(%Q{
DELETE FROM notification_settings
WHERE user_id = #{uid}
AND source_type = #{stype}
AND source_id = #{sid}
AND id != (
SELECT id FROM (
SELECT min(id) AS id
FROM notification_settings
WHERE user_id = #{uid}
AND source_type = #{stype}
AND source_id = #{sid}
) min_ids
)
})
end
end end
end end
class AddAfterSignUpTextToApplicationSettings < ActiveRecord::Migration
def change
add_column :application_settings, :after_sign_up_text, :text
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20160603182247) do ActiveRecord::Schema.define(version: 20160608155312) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -84,6 +84,7 @@ ActiveRecord::Schema.define(version: 20160603182247) do ...@@ -84,6 +84,7 @@ ActiveRecord::Schema.define(version: 20160603182247) do
t.string "health_check_access_token" t.string "health_check_access_token"
t.boolean "send_user_confirmation_email", default: false t.boolean "send_user_confirmation_email", default: false
t.integer "container_registry_token_expire_delay", default: 5 t.integer "container_registry_token_expire_delay", default: 5
t.text "after_sign_up_text"
end end
create_table "audit_events", force: :cascade do |t| create_table "audit_events", force: :cascade do |t|
......
...@@ -49,8 +49,8 @@ information from database or file system ...@@ -49,8 +49,8 @@ information from database or file system
## Buttons ## Buttons
* Button should contain icon or text. Exceptions should be approved by UX designer. * Button should contain icon or text. Exceptions should be approved by UX designer.
* Use gray button on white background or white button on gray background.
* Use red button for destructive actions (not revertable). For example removing issue. * Use red button for destructive actions (not revertable). For example removing issue.
* Use green or blue button for primary action. Primary button should be only one. * Use green or blue button for primary action. Primary button should be only one.
Do not use both green and blue button in one form. Do not use both green and blue button in one form.
* For all other cases use default white button
...@@ -120,7 +120,7 @@ will need to let gitlab-workhorse listen on a TCP port. You can do this ...@@ -120,7 +120,7 @@ will need to let gitlab-workhorse listen on a TCP port. You can do this
via [/etc/default/gitlab]. via [/etc/default/gitlab].
[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache [Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache
[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-8-stable/lib/support/init.d/gitlab.default.example#L37 [/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-9-stable/lib/support/init.d/gitlab.default.example#L37
#### Init script #### Init script
...@@ -145,7 +145,7 @@ To make sure you didn't miss anything run a more thorough check: ...@@ -145,7 +145,7 @@ To make sure you didn't miss anything run a more thorough check:
If all items are green, then congratulations, the upgrade is complete! If all items are green, then congratulations, the upgrade is complete!
## Things went south? Revert to previous version (8.7) ## Things went south? Revert to previous version (8.8)
### 1. Revert the code to the previous version ### 1. Revert the code to the previous version
......
...@@ -9,7 +9,7 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps ...@@ -9,7 +9,7 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps
step 'I remove label \'bug\'' do step 'I remove label \'bug\'' do
page.within "#label_#{bug_label.id}" do page.within "#label_#{bug_label.id}" do
click_link 'Delete' first(:link, 'Delete').click
end end
end end
......
...@@ -29,6 +29,6 @@ class Spinach::Features::Labels < Spinach::FeatureSteps ...@@ -29,6 +29,6 @@ class Spinach::Features::Labels < Spinach::FeatureSteps
private private
def subscribe_button def subscribe_button
first('.label-subscribe-button span') first('.js-subscribe-button', visible: true)
end end
end end
...@@ -97,7 +97,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps ...@@ -97,7 +97,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
file = Gollum::File.new(wiki.wiki) file = Gollum::File.new(wiki.wiki)
Gollum::Wiki.any_instance.stub(:file).with("image.jpg", "master", true).and_return(file) Gollum::Wiki.any_instance.stub(:file).with("image.jpg", "master", true).and_return(file)
Gollum::File.any_instance.stub(:mime_type).and_return("image/jpeg") Gollum::File.any_instance.stub(:mime_type).and_return("image/jpeg")
expect(page).to have_link('image', href: "image.jpg") expect(page).to have_link('image', href: "#{wiki.wiki_base_path}/image.jpg")
click_on "image" click_on "image"
end end
...@@ -113,7 +113,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps ...@@ -113,7 +113,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
end end
step 'I click on image link' do step 'I click on image link' do
expect(page).to have_link('image', href: "image.jpg") expect(page).to have_link('image', href: "#{wiki.wiki_base_path}/image.jpg")
click_on "image" click_on "image"
end end
......
...@@ -351,6 +351,7 @@ module API ...@@ -351,6 +351,7 @@ module API
expose :signin_enabled expose :signin_enabled
expose :gravatar_enabled expose :gravatar_enabled
expose :sign_in_text expose :sign_in_text
expose :after_sign_up_text
expose :created_at expose :created_at
expose :updated_at expose :updated_at
expose :home_page_url expose :home_page_url
......
...@@ -51,7 +51,7 @@ module API ...@@ -51,7 +51,7 @@ module API
# GET /issues?labels=foo,bar # GET /issues?labels=foo,bar
# GET /issues?labels=foo,bar&state=opened # GET /issues?labels=foo,bar&state=opened
get do get do
issues = current_user.issues issues = current_user.issues.inc_notes_with_associations
issues = filter_issues_state(issues, params[:state]) unless params[:state].nil? issues = filter_issues_state(issues, params[:state]) unless params[:state].nil?
issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil? issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil?
issues.reorder(issuable_order_by => issuable_sort) issues.reorder(issuable_order_by => issuable_sort)
...@@ -82,7 +82,7 @@ module API ...@@ -82,7 +82,7 @@ module API
# GET /projects/:id/issues?milestone=1.0.0&state=closed # GET /projects/:id/issues?milestone=1.0.0&state=closed
# GET /issues?iid=42 # GET /issues?iid=42
get ":id/issues" do get ":id/issues" do
issues = user_project.issues.visible_to_user(current_user) issues = user_project.issues.inc_notes_with_associations.visible_to_user(current_user)
issues = filter_issues_state(issues, params[:state]) unless params[:state].nil? issues = filter_issues_state(issues, params[:state]) unless params[:state].nil?
issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil? issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil?
issues = filter_by_iid(issues, params[:iid]) unless params[:iid].nil? issues = filter_by_iid(issues, params[:iid]) unless params[:iid].nil?
......
...@@ -41,7 +41,7 @@ module API ...@@ -41,7 +41,7 @@ module API
# #
get ":id/merge_requests" do get ":id/merge_requests" do
authorize! :read_merge_request, user_project authorize! :read_merge_request, user_project
merge_requests = user_project.merge_requests merge_requests = user_project.merge_requests.inc_notes_with_associations
unless params[:iid].nil? unless params[:iid].nil?
merge_requests = filter_by_iid(merge_requests, params[:iid]) merge_requests = filter_by_iid(merge_requests, params[:iid])
......
...@@ -11,8 +11,7 @@ module API ...@@ -11,8 +11,7 @@ module API
# Example Request: # Example Request:
# POST /session # POST /session
post "/session" do post "/session" do
auth = Gitlab::Auth.new user = Gitlab::Auth.find_in_gitlab_or_ldap(params[:email] || params[:login], params[:password])
user = auth.find(params[:email] || params[:login], params[:password])
return unauthorized! unless user return unauthorized! unless user
present user, with: Entities::UserLogin present user, with: Entities::UserLogin
......
...@@ -15,6 +15,7 @@ module Banzai ...@@ -15,6 +15,7 @@ module Banzai
next if link.start_with?(internal_url) next if link.start_with?(internal_url)
node.set_attribute('rel', 'nofollow noreferrer') node.set_attribute('rel', 'nofollow noreferrer')
node.set_attribute('target', '_blank')
end end
doc doc
......
...@@ -2,7 +2,8 @@ require 'uri' ...@@ -2,7 +2,8 @@ require 'uri'
module Banzai module Banzai
module Filter module Filter
# HTML filter that "fixes" relative links to files in a repository. # HTML filter that "fixes" links to pages/files in a wiki.
# Rewrite rules are documented in the `WikiPipeline` spec.
# #
# Context options: # Context options:
# :project_wiki # :project_wiki
...@@ -25,36 +26,15 @@ module Banzai ...@@ -25,36 +26,15 @@ module Banzai
end end
def process_link_attr(html_attr) def process_link_attr(html_attr)
return if html_attr.blank? || file_reference?(html_attr) || hierarchical_link?(html_attr) return if html_attr.blank?
uri = URI(html_attr.value) html_attr.value = apply_rewrite_rules(html_attr.value)
if uri.relative? && uri.path.present?
html_attr.value = rebuild_wiki_uri(uri).to_s
end
rescue URI::Error rescue URI::Error
# noop # noop
end end
def rebuild_wiki_uri(uri) def apply_rewrite_rules(link_string)
uri.path = ::File.join(project_wiki_base_path, uri.path) Rewriter.new(link_string, wiki: context[:project_wiki], slug: context[:page_slug]).apply_rules
uri
end
def project_wiki
context[:project_wiki]
end
def file_reference?(html_attr)
!File.extname(html_attr.value).blank?
end
# Of the form `./link`, `../link`, or similar
def hierarchical_link?(html_attr)
html_attr.value[0] == '.'
end
def project_wiki_base_path
project_wiki && project_wiki.wiki_base_path
end end
end end
end end
......
module Banzai
module Filter
class WikiLinkFilter < HTML::Pipeline::Filter
class Rewriter
def initialize(link_string, wiki:, slug:)
@uri = Addressable::URI.parse(link_string)
@wiki_base_path = wiki && wiki.wiki_base_path
@slug = slug
end
def apply_rules
apply_file_link_rules!
apply_hierarchical_link_rules!
apply_relative_link_rules!
@uri.to_s
end
private
# Of the form 'file.md'
def apply_file_link_rules!
@uri = Addressable::URI.join(@slug, @uri) if @uri.extname.present?
end
# Of the form `./link`, `../link`, or similar
def apply_hierarchical_link_rules!
@uri = Addressable::URI.join(@slug, @uri) if @uri.to_s[0] == '.'
end
# Any link _not_ of the form `http://example.com/`
def apply_relative_link_rules!
if @uri.relative? && @uri.path.present?
link = ::File.join(@wiki_base_path, @uri.path)
@uri = Addressable::URI.parse(link)
end
end
end
end
end
end
module Gitlab module Gitlab
class Auth module Auth
def find(login, password) Result = Struct.new(:user, :type)
class << self
def find(login, password, project:, ip:)
raise "Must provide an IP for rate limiting" if ip.nil?
result = Result.new
if valid_ci_request?(login, password, project)
result.type = :ci
elsif result.user = find_in_gitlab_or_ldap(login, password)
result.type = :gitlab_or_ldap
elsif result.user = oauth_access_token_check(login, password)
result.type = :oauth
end
rate_limit!(ip, success: !!result.user || (result.type == :ci), login: login)
result
end
def find_in_gitlab_or_ldap(login, password)
user = User.by_login(login) user = User.by_login(login)
# If no user is found, or it's an LDAP server, try LDAP. # If no user is found, or it's an LDAP server, try LDAP.
...@@ -14,5 +34,54 @@ module Gitlab ...@@ -14,5 +34,54 @@ module Gitlab
user if user.valid_password?(password) user if user.valid_password?(password)
end end
end end
def rate_limit!(ip, success:, login:)
rate_limiter = Gitlab::Auth::IpRateLimiter.new(ip)
return unless rate_limiter.enabled?
if success
# Repeated login 'failures' are normal behavior for some Git clients so
# it is important to reset the ban counter once the client has proven
# they are not a 'bad guy'.
rate_limiter.reset!
else
# Register a login failure so that Rack::Attack can block the next
# request from this IP if needed.
rate_limiter.register_fail!
if rate_limiter.banned?
Rails.logger.info "IP #{ip} failed to login " \
"as #{login} but has been temporarily banned from Git auth"
end
end
end
private
def valid_ci_request?(login, password, project)
matched_login = /(?<service>^[a-zA-Z]*-ci)-token$/.match(login)
return false unless project && matched_login.present?
underscored_service = matched_login['service'].underscore
if underscored_service == 'gitlab_ci'
project && project.valid_build_token?(password)
elsif Service.available_services_names.include?(underscored_service)
# We treat underscored_service as a trusted input because it is included
# in the Service.available_services_names whitelist.
service = project.public_send("#{underscored_service}_service")
service && service.activated? && service.valid_token?(password)
end
end
def oauth_access_token_check(login, password)
if login == "oauth2" && password.present?
token = Doorkeeper::AccessToken.by_token(password)
token && token.accessible? && User.find_by(id: token.resource_owner_id)
end
end
end
end end
end end
module Gitlab
module Auth
class IpRateLimiter
attr_reader :ip
def initialize(ip)
@ip = ip
@banned = false
end
def enabled?
config.enabled
end
def reset!
Rack::Attack::Allow2Ban.reset(ip, config)
end
def register_fail!
# Allow2Ban.filter will return false if this IP has not failed too often yet
@banned = Rack::Attack::Allow2Ban.filter(ip, config) do
# If we return false here, the failure for this IP is ignored by Allow2Ban
ip_can_be_banned?
end
end
def banned?
@banned
end
private
def config
Gitlab.config.rack_attack.git_basic_auth
end
def ip_can_be_banned?
config.ip_whitelist.exclude?(ip)
end
end
end
end
...@@ -36,10 +36,7 @@ module Grack ...@@ -36,10 +36,7 @@ module Grack
lfs_response = Gitlab::Lfs::Router.new(project, @user, @request).try_call lfs_response = Gitlab::Lfs::Router.new(project, @user, @request).try_call
return lfs_response unless lfs_response.nil? return lfs_response unless lfs_response.nil?
if project && authorized_request? if @user.nil? && !@ci
# Tell gitlab-workhorse the request is OK, and what the GL_ID is
render_grack_auth_ok
elsif @user.nil? && !@ci
unauthorized unauthorized
else else
render_not_found render_not_found
...@@ -98,7 +95,7 @@ module Grack ...@@ -98,7 +95,7 @@ module Grack
end end
def authenticate_user(login, password) def authenticate_user(login, password)
user = Gitlab::Auth.new.find(login, password) user = Gitlab::Auth.find_in_gitlab_or_ldap(login, password)
unless user unless user
user = oauth_access_token_check(login, password) user = oauth_access_token_check(login, password)
...@@ -141,36 +138,6 @@ module Grack ...@@ -141,36 +138,6 @@ module Grack
user user
end end
def authorized_request?
return true if @ci
case git_cmd
when *Gitlab::GitAccess::DOWNLOAD_COMMANDS
if !Gitlab.config.gitlab_shell.upload_pack
false
elsif user
Gitlab::GitAccess.new(user, project).download_access_check.allowed?
elsif project.public?
# Allow clone/fetch for public projects
true
else
false
end
when *Gitlab::GitAccess::PUSH_COMMANDS
if !Gitlab.config.gitlab_shell.receive_pack
false
elsif user
# Skip user authorization on upload request.
# It will be done by the pre-receive hook in the repository.
true
else
false
end
else
false
end
end
def git_cmd def git_cmd
if @request.get? if @request.get?
@request.params['service'] @request.params['service']
...@@ -197,24 +164,6 @@ module Grack ...@@ -197,24 +164,6 @@ module Grack
end end
end end
def render_grack_auth_ok
repo_path =
if @request.path_info =~ /^([\w\.\/-]+)\.wiki\.git/
ProjectWiki.new(project).repository.path_to_repo
else
project.repository.path_to_repo
end
[
200,
{ "Content-Type" => "application/json" },
[JSON.dump({
'GL_ID' => Gitlab::ShellEnv.gl_id(@user),
'RepoPath' => repo_path,
})]
]
end
def render_not_found def render_not_found
[404, { "Content-Type" => "text/plain" }, ["Not Found"]] [404, { "Content-Type" => "text/plain" }, ["Not Found"]]
end end
......
...@@ -26,7 +26,10 @@ module Gitlab ...@@ -26,7 +26,10 @@ module Gitlab
signup_enabled: Settings.gitlab['signup_enabled'], signup_enabled: Settings.gitlab['signup_enabled'],
signin_enabled: Settings.gitlab['signin_enabled'], signin_enabled: Settings.gitlab['signin_enabled'],
gravatar_enabled: Settings.gravatar['enabled'], gravatar_enabled: Settings.gravatar['enabled'],
sign_in_text: Settings.extra['sign_in_text'], sign_in_text: nil,
after_sign_up_text: nil,
help_page_text: nil,
shared_runners_text: nil,
restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'], restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'],
max_attachment_size: Settings.gitlab['max_attachment_size'], max_attachment_size: Settings.gitlab['max_attachment_size'],
session_expire_delay: Settings.gitlab['session_expire_delay'], session_expire_delay: Settings.gitlab['session_expire_delay'],
......
...@@ -146,6 +146,7 @@ module Gitlab ...@@ -146,6 +146,7 @@ module Gitlab
def update_webhooks(hooks, options) def update_webhooks(hooks, options)
hooks.each do |hook| hooks.each do |hook|
sleep rate_limit_sleep_time if rate_limit_exceed?
client.edit_hook(repo, hook.id, hook.name, hook.config, options) client.edit_hook(repo, hook.id, hook.name, hook.config, options)
end end
end end
......
...@@ -8,6 +8,7 @@ module Gitlab ...@@ -8,6 +8,7 @@ module Gitlab
gon.relative_url_root = Gitlab.config.gitlab.relative_url_root gon.relative_url_root = Gitlab.config.gitlab.relative_url_root
gon.shortcuts_path = help_shortcuts_path gon.shortcuts_path = help_shortcuts_path
gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class
gon.award_menu_url = emojis_path
if current_user if current_user
gon.current_user_id = current_user.id gon.current_user_id = current_user.id
......
...@@ -69,13 +69,20 @@ module Gitlab ...@@ -69,13 +69,20 @@ module Gitlab
return unless ldap_person return unless ldap_person
# If a corresponding person exists with same uid in a LDAP server, # If a corresponding person exists with same uid in a LDAP server,
# set up a Gitlab user with dual LDAP and Omniauth identities. # check if the user already has a GitLab account.
if user = Gitlab::LDAP::User.find_by_uid_and_provider(ldap_person.dn, ldap_person.provider) user = Gitlab::LDAP::User.find_by_uid_and_provider(ldap_person.dn, ldap_person.provider)
# Case when a LDAP user already exists in Gitlab. Add the Omniauth identity to existing account. if user
# Case when a LDAP user already exists in Gitlab. Add the OAuth identity to existing account.
log.info "LDAP account found for user #{user.username}. Building new #{auth_hash.provider} identity."
user.identities.build(extern_uid: auth_hash.uid, provider: auth_hash.provider) user.identities.build(extern_uid: auth_hash.uid, provider: auth_hash.provider)
else else
# No account in Gitlab yet: create it and add the LDAP identity log.info "No existing LDAP account was found in GitLab. Checking for #{auth_hash.provider} account."
user = find_by_uid_and_provider
if user.nil?
log.info "No user found using #{auth_hash.provider} provider. Creating a new one."
user = build_new_user user = build_new_user
end
log.info "Correct account has been found. Adding LDAP identity to user: #{user.username}."
user.identities.new(provider: ldap_person.provider, extern_uid: ldap_person.dn) user.identities.new(provider: ldap_person.provider, extern_uid: ldap_person.dn)
end end
......
...@@ -12,12 +12,12 @@ module Gitlab ...@@ -12,12 +12,12 @@ module Gitlab
end end
def gl_user def gl_user
@user ||= find_by_uid_and_provider
if auto_link_ldap_user? if auto_link_ldap_user?
@user ||= find_or_create_ldap_user @user ||= find_or_create_ldap_user
end end
@user ||= find_by_uid_and_provider
if auto_link_saml_user? if auto_link_saml_user?
@user ||= find_by_email @user ||= find_by_email
end end
......
...@@ -6,6 +6,13 @@ module Gitlab ...@@ -6,6 +6,13 @@ module Gitlab
SEND_DATA_HEADER = 'Gitlab-Workhorse-Send-Data' SEND_DATA_HEADER = 'Gitlab-Workhorse-Send-Data'
class << self class << self
def git_http_ok(repository, user)
{
'GL_ID' => Gitlab::ShellEnv.gl_id(user),
'RepoPath' => repository.path_to_repo,
}
end
def send_git_blob(repository, blob) def send_git_blob(repository, blob)
params = { params = {
'RepoPath' => repository.path_to_repo, 'RepoPath' => repository.path_to_repo,
......
...@@ -34,7 +34,7 @@ namespace :gitlab do ...@@ -34,7 +34,7 @@ namespace :gitlab do
# PG: http://www.postgresql.org/docs/current/static/ddl-depend.html # PG: http://www.postgresql.org/docs/current/static/ddl-depend.html
# MySQL: http://dev.mysql.com/doc/refman/5.7/en/drop-table.html # MySQL: http://dev.mysql.com/doc/refman/5.7/en/drop-table.html
# Add `IF EXISTS` because cascade could have already deleted a table. # Add `IF EXISTS` because cascade could have already deleted a table.
tables.each { |t| connection.execute("DROP TABLE IF EXISTS #{t} CASCADE") } tables.each { |t| connection.execute("DROP TABLE IF EXISTS #{connection.quote_table_name(t)} CASCADE") }
end end
desc 'Configures the database by running migrate, or by loading the schema and seeding if needed' desc 'Configures the database by running migrate, or by loading the schema and seeding if needed'
......
require 'spec_helper' require 'spec_helper'
require_relative 'import_spec_helper'
describe Import::BitbucketController do describe Import::BitbucketController do
include ImportSpecHelper include ImportSpecHelper
......
require 'spec_helper' require 'spec_helper'
require_relative 'import_spec_helper'
describe Import::FogbugzController do describe Import::FogbugzController do
include ImportSpecHelper include ImportSpecHelper
......
require 'spec_helper' require 'spec_helper'
require_relative 'import_spec_helper'
describe Import::GithubController do describe Import::GithubController do
include ImportSpecHelper include ImportSpecHelper
......
require 'spec_helper' require 'spec_helper'
require_relative 'import_spec_helper'
describe Import::GitlabController do describe Import::GitlabController do
include ImportSpecHelper include ImportSpecHelper
......
require 'spec_helper' require 'spec_helper'
require_relative 'import_spec_helper'
describe Import::GitoriousController do describe Import::GitoriousController do
include ImportSpecHelper include ImportSpecHelper
......
require 'spec_helper' require 'spec_helper'
require_relative 'import_spec_helper'
describe Import::GoogleCodeController do describe Import::GoogleCodeController do
include ImportSpecHelper include ImportSpecHelper
......
require 'spec_helper'
describe Oauth::ApplicationsController do
let(:user) { create(:user) }
context 'project members' do
before do
sign_in(user)
end
describe 'GET #index' do
it 'shows list of applications' do
get :index
expect(response.status).to eq(200)
end
it 'redirects back to profile page if OAuth applications are disabled' do
settings = double(user_oauth_applications?: false)
allow_any_instance_of(Gitlab::CurrentSettings).to receive(:current_application_settings).and_return(settings)
get :index
expect(response.status).to eq(302)
expect(response).to redirect_to(profile_path)
end
end
end
end
...@@ -2,7 +2,7 @@ require 'ostruct' ...@@ -2,7 +2,7 @@ require 'ostruct'
FactoryGirl.define do FactoryGirl.define do
factory :wiki_page do factory :wiki_page do
page = OpenStruct.new(url_path: 'some-name') page { OpenStruct.new(url_path: 'some-name') }
association :wiki, factory: :project_wiki, strategy: :build association :wiki, factory: :project_wiki, strategy: :build
initialize_with { new(wiki, page, true) } initialize_with { new(wiki, page, true) }
end end
......
require 'spec_helper'
feature 'Tooltips on .timeago dates', feature: true, js: true do
include WaitForAjax
let(:user) { create(:user) }
let(:project) { create(:project, name: 'test', namespace: user.namespace) }
let(:created_date) { Date.yesterday.to_time }
let(:expected_format) { created_date.strftime('%b %-d, %Y %l:%M%P UTC') }
context 'on the activity tab' do
before do
project.team << [user, :master]
Event.create( project: project, author_id: user.id, action: Event::JOINED,
updated_at: created_date, created_at: created_date)
login_as user
visit user_path(user)
wait_for_ajax()
page.find('.js-timeago').hover
end
it 'has the datetime formated correctly' do
expect(page).to have_selector('.local-timeago', text: expected_format)
end
end
context 'on the snippets tab' do
before do
project.team << [user, :master]
create(:snippet, author: user, updated_at: created_date, created_at: created_date)
login_as user
visit user_snippets_path(user)
wait_for_ajax()
page.find('.js-timeago').hover
end
it 'has the datetime formated correctly' do
expect(page).to have_selector('.local-timeago', text: expected_format)
end
end
end
...@@ -54,6 +54,11 @@ feature 'Issue filtering by Labels', feature: true do ...@@ -54,6 +54,11 @@ feature 'Issue filtering by Labels', feature: true do
expect(find('.filtered-labels')).not_to have_content "feature" expect(find('.filtered-labels')).not_to have_content "feature"
expect(find('.filtered-labels')).not_to have_content "enhancement" expect(find('.filtered-labels')).not_to have_content "enhancement"
end end
it 'should remove label "bug"' do
first('.js-label-filter-remove').click
expect(find('.filtered-labels')).to have_no_content "bug"
end
end end
context 'filter by label feature', js: true do context 'filter by label feature', js: true do
...@@ -135,6 +140,11 @@ feature 'Issue filtering by Labels', feature: true do ...@@ -135,6 +140,11 @@ feature 'Issue filtering by Labels', feature: true do
it 'should not show label "bug" in filtered-labels' do it 'should not show label "bug" in filtered-labels' do
expect(find('.filtered-labels')).not_to have_content "bug" expect(find('.filtered-labels')).not_to have_content "bug"
end end
it 'should remove label "enhancement"' do
first('.js-label-filter-remove').click
expect(find('.filtered-labels')).to have_no_content "enhancement"
end
end end
context 'filter by label enhancement and bug in issues list', js: true do context 'filter by label enhancement and bug in issues list', js: true do
...@@ -164,4 +174,29 @@ feature 'Issue filtering by Labels', feature: true do ...@@ -164,4 +174,29 @@ feature 'Issue filtering by Labels', feature: true do
expect(find('.filtered-labels')).not_to have_content "feature" expect(find('.filtered-labels')).not_to have_content "feature"
end end
end end
context 'remove filtered labels', js: true do
before do
page.within '.labels-filter' do
click_button 'Label'
click_link 'bug'
find('.dropdown-menu-close').click
end
page.within '.filtered-labels' do
expect(page).to have_content 'bug'
end
end
it 'should allow user to remove filtered labels' do
page.within '.filtered-labels' do
first('.js-label-filter-remove').click
expect(page).not_to have_content 'bug'
end
page.within '.labels-filter' do
expect(page).not_to have_content 'bug'
end
end
end
end end
...@@ -75,12 +75,13 @@ describe 'Issues', feature: true do ...@@ -75,12 +75,13 @@ describe 'Issues', feature: true do
fill_in 'issue_title', with: 'bug 345' fill_in 'issue_title', with: 'bug 345'
fill_in 'issue_description', with: 'bug description' fill_in 'issue_description', with: 'bug description'
find('#issuable-due-date').click
page.within '.datepicker' do page.within '.ui-datepicker' do
click_link date.day click_link date.day
end end
expect(find('#issuable-due-date', visible: false).value).to eq date.to_s expect(find('#issuable-due-date').value).to eq date.to_s
click_button 'Submit issue' click_button 'Submit issue'
...@@ -100,18 +101,19 @@ describe 'Issues', feature: true do ...@@ -100,18 +101,19 @@ describe 'Issues', feature: true do
it 'should save with due date' do it 'should save with due date' do
date = Date.today.at_beginning_of_month date = Date.today.at_beginning_of_month
expect(find('#issuable-due-date', visible: false).value).to eq date.to_s expect(find('#issuable-due-date').value).to eq date.to_s
date = date.tomorrow date = date.tomorrow
fill_in 'issue_title', with: 'bug 345' fill_in 'issue_title', with: 'bug 345'
fill_in 'issue_description', with: 'bug description' fill_in 'issue_description', with: 'bug description'
find('#issuable-due-date').click
page.within '.datepicker' do page.within '.ui-datepicker' do
click_link date.day click_link date.day
end end
expect(find('#issuable-due-date', visible: false).value).to eq date.to_s expect(find('#issuable-due-date').value).to eq date.to_s
click_button 'Save changes' click_button 'Save changes'
......
...@@ -165,22 +165,32 @@ describe 'GitLab Markdown', feature: true do ...@@ -165,22 +165,32 @@ describe 'GitLab Markdown', feature: true do
describe 'ExternalLinkFilter' do describe 'ExternalLinkFilter' do
it 'adds nofollow to external link' do it 'adds nofollow to external link' do
link = doc.at_css('a:contains("Google")') link = doc.at_css('a:contains("Google")')
expect(link.attr('rel')).to include('nofollow') expect(link.attr('rel')).to include('nofollow')
end end
it 'adds noreferrer to external link' do it 'adds noreferrer to external link' do
link = doc.at_css('a:contains("Google")') link = doc.at_css('a:contains("Google")')
expect(link.attr('rel')).to include('noreferrer') expect(link.attr('rel')).to include('noreferrer')
end end
it 'adds _blank to target attribute for external links' do
link = doc.at_css('a:contains("Google")')
expect(link.attr('target')).to match('_blank')
end
it 'ignores internal link' do it 'ignores internal link' do
link = doc.at_css('a:contains("GitLab Root")') link = doc.at_css('a:contains("GitLab Root")')
expect(link.attr('rel')).not_to match 'nofollow' expect(link.attr('rel')).not_to match 'nofollow'
expect(link.attr('target')).not_to match '_blank'
end end
end end
end end
before(:all) do before do
@feat = MarkdownFeature.new @feat = MarkdownFeature.new
# `markdown` helper expects a `@project` variable # `markdown` helper expects a `@project` variable
...@@ -188,7 +198,7 @@ describe 'GitLab Markdown', feature: true do ...@@ -188,7 +198,7 @@ describe 'GitLab Markdown', feature: true do
end end
context 'default pipeline' do context 'default pipeline' do
before(:all) do before do
@html = markdown(@feat.raw_markdown) @html = markdown(@feat.raw_markdown)
end end
...@@ -231,13 +241,14 @@ describe 'GitLab Markdown', feature: true do ...@@ -231,13 +241,14 @@ describe 'GitLab Markdown', feature: true do
context 'wiki pipeline' do context 'wiki pipeline' do
before do before do
@project_wiki = @feat.project_wiki @project_wiki = @feat.project_wiki
@project_wiki_page = @feat.project_wiki_page
file = Gollum::File.new(@project_wiki.wiki) file = Gollum::File.new(@project_wiki.wiki)
expect(file).to receive(:path).and_return('images/example.jpg') expect(file).to receive(:path).and_return('images/example.jpg')
expect(@project_wiki).to receive(:find_file).with('images/example.jpg').and_return(file) expect(@project_wiki).to receive(:find_file).with('images/example.jpg').and_return(file)
allow(@project_wiki).to receive(:wiki_base_path) { '/namespace1/gitlabhq/wikis' } allow(@project_wiki).to receive(:wiki_base_path) { '/namespace1/gitlabhq/wikis' }
@html = markdown(@feat.raw_markdown, { pipeline: :wiki, project_wiki: @project_wiki }) @html = markdown(@feat.raw_markdown, { pipeline: :wiki, project_wiki: @project_wiki, page_slug: @project_wiki_page.slug })
end end
it_behaves_like 'all pipelines' it_behaves_like 'all pipelines'
......
...@@ -136,7 +136,7 @@ But it shouldn't autolink text inside certain tags: ...@@ -136,7 +136,7 @@ But it shouldn't autolink text inside certain tags:
### ExternalLinkFilter ### ExternalLinkFilter
External links get a `rel="nofollow"` attribute: External links get a `rel="nofollow noreferrer"` and `target="_blank"` attributes:
- [Google](https://google.com/) - [Google](https://google.com/)
- [GitLab Root](<%= Gitlab.config.gitlab.url %>) - [GitLab Root](<%= Gitlab.config.gitlab.url %>)
......
...@@ -121,13 +121,14 @@ describe GitlabMarkdownHelper do ...@@ -121,13 +121,14 @@ describe GitlabMarkdownHelper do
before do before do
@wiki = double('WikiPage') @wiki = double('WikiPage')
allow(@wiki).to receive(:content).and_return('wiki content') allow(@wiki).to receive(:content).and_return('wiki content')
allow(@wiki).to receive(:slug).and_return('nested/page')
helper.instance_variable_set(:@project_wiki, @wiki) helper.instance_variable_set(:@project_wiki, @wiki)
end end
it "should use Wiki pipeline for markdown files" do it "should use Wiki pipeline for markdown files" do
allow(@wiki).to receive(:format).and_return(:markdown) allow(@wiki).to receive(:format).and_return(:markdown)
expect(helper).to receive(:markdown).with('wiki content', pipeline: :wiki, project_wiki: @wiki) expect(helper).to receive(:markdown).with('wiki content', pipeline: :wiki, project_wiki: @wiki, page_slug: "nested/page")
helper.render_wiki_content(@wiki) helper.render_wiki_content(@wiki)
end end
......
...@@ -5,8 +5,9 @@ ...@@ -5,8 +5,9 @@
awardsHandler = null awardsHandler = null
window.gl or= {} window.gl or= {}
window.gon or= {}
gl.emojiAliases = -> return { '+1': 'thumbsup', '-1': 'thumbsdown' } gl.emojiAliases = -> return { '+1': 'thumbsup', '-1': 'thumbsdown' }
gl.awardMenuUrl = '/emojis' gon.award_menu_url = '/emojis'
lazyAssert = (done, assertFn) -> lazyAssert = (done, assertFn) ->
...@@ -25,9 +26,7 @@ describe 'AwardsHandler', -> ...@@ -25,9 +26,7 @@ describe 'AwardsHandler', ->
fixture.load 'awards_handler.html' fixture.load 'awards_handler.html'
awardsHandler = new AwardsHandler awardsHandler = new AwardsHandler
spyOn(awardsHandler, 'postEmoji').and.callFake (url, emoji, cb) => cb() spyOn(awardsHandler, 'postEmoji').and.callFake (url, emoji, cb) => cb()
spyOn(jQuery, 'get').and.callFake (req, cb) -> spyOn(jQuery, 'get').and.callFake (req, cb) -> cb window.emojiMenu
expect(req).toBe '/emojis'
cb window.emojiMenu
describe '::showEmojiMenu', -> describe '::showEmojiMenu', ->
......
require 'spec_helper'
describe Banzai::Filter::WikiLinkFilter, lib: true do
include FilterSpecHelper
let(:namespace) { build_stubbed(:namespace, name: "wiki_link_ns") }
let(:project) { build_stubbed(:empty_project, :public, name: "wiki_link_project", namespace: namespace) }
let(:user) { double }
let(:project_wiki) { ProjectWiki.new(project, user) }
describe "links within the wiki (relative)" do
describe "hierarchical links to the current directory" do
it "doesn't rewrite non-file links" do
link = "<a href='./page'>Link to Page</a>"
filtered_link = filter(link, project_wiki: project_wiki).children[0]
expect(filtered_link.attribute('href').value).to eq('./page')
end
it "doesn't rewrite file links" do
link = "<a href='./page.md'>Link to Page</a>"
filtered_link = filter(link, project_wiki: project_wiki).children[0]
expect(filtered_link.attribute('href').value).to eq('./page.md')
end
end
describe "hierarchical links to the parent directory" do
it "doesn't rewrite non-file links" do
link = "<a href='../page'>Link to Page</a>"
filtered_link = filter(link, project_wiki: project_wiki).children[0]
expect(filtered_link.attribute('href').value).to eq('../page')
end
it "doesn't rewrite file links" do
link = "<a href='../page.md'>Link to Page</a>"
filtered_link = filter(link, project_wiki: project_wiki).children[0]
expect(filtered_link.attribute('href').value).to eq('../page.md')
end
end
describe "hierarchical links to a sub-directory" do
it "doesn't rewrite non-file links" do
link = "<a href='./subdirectory/page'>Link to Page</a>"
filtered_link = filter(link, project_wiki: project_wiki).children[0]
expect(filtered_link.attribute('href').value).to eq('./subdirectory/page')
end
it "doesn't rewrite file links" do
link = "<a href='./subdirectory/page.md'>Link to Page</a>"
filtered_link = filter(link, project_wiki: project_wiki).children[0]
expect(filtered_link.attribute('href').value).to eq('./subdirectory/page.md')
end
end
describe "non-hierarchical links" do
it 'rewrites non-file links to be at the scope of the wiki root' do
link = "<a href='page'>Link to Page</a>"
filtered_link = filter(link, project_wiki: project_wiki).children[0]
expect(filtered_link.attribute('href').value).to match('/wiki_link_ns/wiki_link_project/wikis/page')
end
it "doesn't rewrite file links" do
link = "<a href='page.md'>Link to Page</a>"
filtered_link = filter(link, project_wiki: project_wiki).children[0]
expect(filtered_link.attribute('href').value).to eq('page.md')
end
end
end
describe "links outside the wiki (absolute)" do
it "doesn't rewrite links" do
link = "<a href='http://example.com/page'>Link to Page</a>"
filtered_link = filter(link, project_wiki: project_wiki).children[0]
expect(filtered_link.attribute('href').value).to eq('http://example.com/page')
end
end
end
...@@ -50,4 +50,112 @@ describe Banzai::Pipeline::WikiPipeline do ...@@ -50,4 +50,112 @@ describe Banzai::Pipeline::WikiPipeline do
end end
end end
end end
describe "Links" do
let(:namespace) { build_stubbed(:namespace, name: "wiki_link_ns") }
let(:project) { build_stubbed(:empty_project, :public, name: "wiki_link_project", namespace: namespace) }
let(:project_wiki) { ProjectWiki.new(project, double(:user)) }
let(:page) { build(:wiki_page, wiki: project_wiki, page: OpenStruct.new(url_path: 'nested/twice/start-page')) }
{ "when GitLab is hosted at a root URL" => '/',
"when GitLab is hosted at a relative URL" => '/nested/relative/gitlab' }.each do |test_name, relative_url_root|
context test_name do
before do
allow(Gitlab.config.gitlab).to receive(:relative_url_root).and_return(relative_url_root)
end
describe "linking to pages within the wiki" do
context "when creating hierarchical links to the current directory" do
it "rewrites non-file links to be at the scope of the current directory" do
markdown = "[Page](./page)"
output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/twice/page\"")
end
it "rewrites file links to be at the scope of the current directory" do
markdown = "[Link to Page](./page.md)"
output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/twice/page.md\"")
end
end
context "when creating hierarchical links to the parent directory" do
it "rewrites non-file links to be at the scope of the parent directory" do
markdown = "[Link to Page](../page)"
output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/page\"")
end
it "rewrites file links to be at the scope of the parent directory" do
markdown = "[Link to Page](../page.md)"
output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/page.md\"")
end
end
context "when creating hierarchical links to a sub-directory" do
it "rewrites non-file links to be at the scope of the sub-directory" do
markdown = "[Link to Page](./subdirectory/page)"
output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/twice/subdirectory/page\"")
end
it "rewrites file links to be at the scope of the sub-directory" do
markdown = "[Link to Page](./subdirectory/page.md)"
output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/twice/subdirectory/page.md\"")
end
end
describe "when creating non-hierarchical links" do
it 'rewrites non-file links to be at the scope of the wiki root' do
markdown = "[Link to Page](page)"
output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/page\"")
end
it "rewrites file links to be at the scope of the current directory" do
markdown = "[Link to Page](page.md)"
output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/twice/page.md\"")
end
end
describe "when creating root links" do
it 'rewrites non-file links to be at the scope of the wiki root' do
markdown = "[Link to Page](/page)"
output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/page\"")
end
it 'rewrites file links to be at the scope of the wiki root' do
markdown = "[Link to Page](/page.md)"
output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/page.md\"")
end
end
end
describe "linking to pages outside the wiki (absolute)" do
it "doesn't rewrite links" do
markdown = "[Link to Page](http://example.com/page)"
output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
expect(output).to include('href="http://example.com/page"')
end
end
end
end
end
end end
...@@ -2,7 +2,7 @@ require 'spec_helper' ...@@ -2,7 +2,7 @@ require 'spec_helper'
describe DisableEmailInterceptor, lib: true do describe DisableEmailInterceptor, lib: true do
before do before do
ActionMailer::Base.register_interceptor(DisableEmailInterceptor) Mail.register_interceptor(DisableEmailInterceptor)
end end
it 'should not send emails' do it 'should not send emails' do
...@@ -14,7 +14,7 @@ describe DisableEmailInterceptor, lib: true do ...@@ -14,7 +14,7 @@ describe DisableEmailInterceptor, lib: true do
# Removing interceptor from the list because unregister_interceptor is # Removing interceptor from the list because unregister_interceptor is
# implemented in later version of mail gem # implemented in later version of mail gem
# See: https://github.com/mikel/mail/pull/705 # See: https://github.com/mikel/mail/pull/705
Mail.class_variable_set(:@@delivery_interceptors, []) Mail.unregister_interceptor(DisableEmailInterceptor)
end end
def deliver_mail def deliver_mail
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::Auth, lib: true do describe Gitlab::Auth, lib: true do
let(:gl_auth) { Gitlab::Auth.new } let(:gl_auth) { described_class }
describe :find do describe 'find' do
it 'recognizes CI' do
token = '123'
project = create(:empty_project)
project.update_attributes(runners_token: token, builds_enabled: true)
ip = 'ip'
expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'gitlab-ci-token')
expect(gl_auth.find('gitlab-ci-token', token, project: project, ip: ip)).to eq(Gitlab::Auth::Result.new(nil, :ci))
end
it 'recognizes master passwords' do
user = create(:user, password: 'password')
ip = 'ip'
expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: user.username)
expect(gl_auth.find(user.username, 'password', project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, :gitlab_or_ldap))
end
it 'recognizes OAuth tokens' do
user = create(:user)
application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user)
token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id)
ip = 'ip'
expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'oauth2')
expect(gl_auth.find("oauth2", token.token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, :oauth))
end
it 'returns double nil for invalid credentials' do
login = 'foo'
ip = 'ip'
expect(gl_auth).to receive(:rate_limit!).with(ip, success: false, login: login)
expect(gl_auth.find(login, 'bar', project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new)
end
end
describe 'find_in_gitlab_or_ldap' do
let!(:user) do let!(:user) do
create(:user, create(:user,
username: username, username: username,
...@@ -14,25 +52,25 @@ describe Gitlab::Auth, lib: true do ...@@ -14,25 +52,25 @@ describe Gitlab::Auth, lib: true do
let(:password) { 'my-secret' } let(:password) { 'my-secret' }
it "should find user by valid login/password" do it "should find user by valid login/password" do
expect( gl_auth.find(username, password) ).to eql user expect( gl_auth.find_in_gitlab_or_ldap(username, password) ).to eql user
end end
it 'should find user by valid email/password with case-insensitive email' do it 'should find user by valid email/password with case-insensitive email' do
expect(gl_auth.find(user.email.upcase, password)).to eql user expect(gl_auth.find_in_gitlab_or_ldap(user.email.upcase, password)).to eql user
end end
it 'should find user by valid username/password with case-insensitive username' do it 'should find user by valid username/password with case-insensitive username' do
expect(gl_auth.find(username.upcase, password)).to eql user expect(gl_auth.find_in_gitlab_or_ldap(username.upcase, password)).to eql user
end end
it "should not find user with invalid password" do it "should not find user with invalid password" do
password = 'wrong' password = 'wrong'
expect( gl_auth.find(username, password) ).not_to eql user expect( gl_auth.find_in_gitlab_or_ldap(username, password) ).not_to eql user
end end
it "should not find user with invalid login" do it "should not find user with invalid login" do
user = 'wrong' user = 'wrong'
expect( gl_auth.find(username, password) ).not_to eql user expect( gl_auth.find_in_gitlab_or_ldap(username, password) ).not_to eql user
end end
context "with ldap enabled" do context "with ldap enabled" do
...@@ -43,13 +81,13 @@ describe Gitlab::Auth, lib: true do ...@@ -43,13 +81,13 @@ describe Gitlab::Auth, lib: true do
it "tries to autheticate with db before ldap" do it "tries to autheticate with db before ldap" do
expect(Gitlab::LDAP::Authentication).not_to receive(:login) expect(Gitlab::LDAP::Authentication).not_to receive(:login)
gl_auth.find(username, password) gl_auth.find_in_gitlab_or_ldap(username, password)
end end
it "uses ldap as fallback to for authentication" do it "uses ldap as fallback to for authentication" do
expect(Gitlab::LDAP::Authentication).to receive(:login) expect(Gitlab::LDAP::Authentication).to receive(:login)
gl_auth.find('ldap_user', 'password') gl_auth.find_in_gitlab_or_ldap('ldap_user', 'password')
end end
end end
end end
......
require "spec_helper"
describe Grack::Auth, lib: true do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:app) { lambda { |env| [200, {}, "Success!"] } }
let!(:auth) { Grack::Auth.new(app) }
let(:env) do
{
'rack.input' => '',
'REQUEST_METHOD' => 'GET',
'QUERY_STRING' => 'service=git-upload-pack'
}
end
let(:status) { auth.call(env).first }
describe "#call" do
context "when the project doesn't exist" do
before do
env["PATH_INFO"] = "doesnt/exist.git"
end
context "when no authentication is provided" do
it "responds with status 401" do
expect(status).to eq(401)
end
end
context "when username and password are provided" do
context "when authentication fails" do
before do
env["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Basic.encode_credentials(user.username, "nope")
end
it "responds with status 401" do
expect(status).to eq(401)
end
end
context "when authentication succeeds" do
before do
env["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password)
end
it "responds with status 404" do
expect(status).to eq(404)
end
end
end
end
context "when the Wiki for a project exists" do
before do
@wiki = ProjectWiki.new(project)
env["PATH_INFO"] = "#{@wiki.repository.path_with_namespace}.git/info/refs"
project.update_attribute(:visibility_level, Project::PUBLIC)
end
it "responds with the right project" do
response = auth.call(env)
json_body = ActiveSupport::JSON.decode(response[2][0])
expect(response.first).to eq(200)
expect(json_body['RepoPath']).to include(@wiki.repository.path_with_namespace)
end
end
context "when the project exists" do
before do
env["PATH_INFO"] = project.path_with_namespace + ".git"
end
context "when the project is public" do
before do
project.update_attribute(:visibility_level, Project::PUBLIC)
end
it "responds with status 200" do
expect(status).to eq(200)
end
end
context "when the project is private" do
before do
project.update_attribute(:visibility_level, Project::PRIVATE)
end
context "when no authentication is provided" do
it "responds with status 401" do
expect(status).to eq(401)
end
end
context "when username and password are provided" do
context "when authentication fails" do
before do
env["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Basic.encode_credentials(user.username, "nope")
end
it "responds with status 401" do
expect(status).to eq(401)
end
context "when the user is IP banned" do
before do
expect(Rack::Attack::Allow2Ban).to receive(:filter).and_return(true)
allow_any_instance_of(Rack::Request).to receive(:ip).and_return('1.2.3.4')
end
it "responds with status 401" do
expect(status).to eq(401)
end
end
end
context "when authentication succeeds" do
before do
env["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password)
end
context "when the user has access to the project" do
before do
project.team << [user, :master]
end
context "when the user is blocked" do
before do
user.block
project.team << [user, :master]
end
it "responds with status 404" do
expect(status).to eq(404)
end
end
context "when the user isn't blocked" do
before do
expect(Rack::Attack::Allow2Ban).to receive(:reset)
end
it "responds with status 200" do
expect(status).to eq(200)
end
end
context "when blank password attempts follow a valid login" do
let(:options) { Gitlab.config.rack_attack.git_basic_auth }
let(:maxretry) { options[:maxretry] - 1 }
let(:ip) { '1.2.3.4' }
before do
allow_any_instance_of(Rack::Request).to receive(:ip).and_return(ip)
Rack::Attack::Allow2Ban.reset(ip, options)
end
after do
Rack::Attack::Allow2Ban.reset(ip, options)
end
def attempt_login(include_password)
password = include_password ? user.password : ""
env["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Basic.encode_credentials(user.username, password)
Grack::Auth.new(app)
auth.call(env).first
end
it "repeated attempts followed by successful attempt" do
maxretry.times.each do
expect(attempt_login(false)).to eq(401)
end
expect(attempt_login(true)).to eq(200)
expect(Rack::Attack::Allow2Ban.banned?(ip)).to be_falsey
maxretry.times.each do
expect(attempt_login(false)).to eq(401)
end
end
end
end
context "when the user doesn't have access to the project" do
it "responds with status 404" do
expect(status).to eq(404)
end
end
end
end
context "when a gitlab ci token is provided" do
let(:token) { "123" }
let(:project) { FactoryGirl.create :empty_project }
before do
project.update_attributes(runners_token: token, builds_enabled: true)
env["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Basic.encode_credentials("gitlab-ci-token", token)
end
it "responds with status 200" do
expect(status).to eq(200)
end
end
end
end
end
end
require 'spec_helper' require 'spec_helper'
describe Gitlab::BitbucketImport::Client, lib: true do describe Gitlab::BitbucketImport::Client, lib: true do
include ImportSpecHelper
let(:token) { '123456' } let(:token) { '123456' }
let(:secret) { 'secret' } let(:secret) { 'secret' }
let(:client) { Gitlab::BitbucketImport::Client.new(token, secret) } let(:client) { Gitlab::BitbucketImport::Client.new(token, secret) }
before do before do
Gitlab.config.omniauth.providers << OpenStruct.new(app_id: "asd123", app_secret: "asd123", name: "bitbucket") stub_omniauth_provider('bitbucket')
end end
it 'all OAuth client options are symbols' do it 'all OAuth client options are symbols' do
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::BitbucketImport::Importer, lib: true do describe Gitlab::BitbucketImport::Importer, lib: true do
include ImportSpecHelper
before do before do
Gitlab.config.omniauth.providers << OpenStruct.new(app_id: "asd123", app_secret: "asd123", name: "bitbucket") stub_omniauth_provider('bitbucket')
end end
let(:statuses) do let(:statuses) do
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::GitlabImport::Client, lib: true do describe Gitlab::GitlabImport::Client, lib: true do
include ImportSpecHelper
let(:token) { '123456' } let(:token) { '123456' }
let(:client) { Gitlab::GitlabImport::Client.new(token) } let(:client) { Gitlab::GitlabImport::Client.new(token) }
before do before do
Gitlab.config.omniauth.providers << OpenStruct.new(app_id: "asd123", app_secret: "asd123", name: "gitlab") stub_omniauth_provider('gitlab')
end end
it 'all OAuth2 client options are symbols' do it 'all OAuth2 client options are symbols' do
......
...@@ -145,6 +145,7 @@ describe Gitlab::Saml::User, lib: true do ...@@ -145,6 +145,7 @@ describe Gitlab::Saml::User, lib: true do
allow(ldap_user).to receive(:email) { %w(john@mail.com john2@example.com) } allow(ldap_user).to receive(:email) { %w(john@mail.com john2@example.com) }
allow(ldap_user).to receive(:dn) { 'uid=user1,ou=People,dc=example' } allow(ldap_user).to receive(:dn) { 'uid=user1,ou=People,dc=example' }
allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user) allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user)
allow(Gitlab::LDAP::Person).to receive(:find_by_dn).and_return(ldap_user)
end end
context 'and no account for the LDAP user' do context 'and no account for the LDAP user' do
...@@ -177,6 +178,23 @@ describe Gitlab::Saml::User, lib: true do ...@@ -177,6 +178,23 @@ describe Gitlab::Saml::User, lib: true do
]) ])
end end
end end
context 'user has SAML user, and wants to add their LDAP identity' do
it 'adds the LDAP identity to the existing SAML user' do
create(:omniauth_user, email: 'john@mail.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'saml', username: 'john')
local_hash = OmniAuth::AuthHash.new(uid: 'uid=user1,ou=People,dc=example', provider: provider, info: info_hash)
local_saml_user = described_class.new(local_hash)
local_saml_user.save
local_gl_user = local_saml_user.gl_user
expect(local_gl_user).to be_valid
expect(local_gl_user.identities.length).to eql 2
identities_as_hash = local_gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
expect(identities_as_hash).to match_array([ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
{ provider: 'saml', extern_uid: 'uid=user1,ou=People,dc=example' }
])
end
end
end end
end end
end end
......
...@@ -10,6 +10,16 @@ describe Issue, "Issuable" do ...@@ -10,6 +10,16 @@ describe Issue, "Issuable" do
it { is_expected.to belong_to(:assignee) } it { is_expected.to belong_to(:assignee) }
it { is_expected.to have_many(:notes).dependent(:destroy) } it { is_expected.to have_many(:notes).dependent(:destroy) }
it { is_expected.to have_many(:todos).dependent(:destroy) } it { is_expected.to have_many(:todos).dependent(:destroy) }
context 'Notes' do
let!(:note) { create(:note, noteable: issue, project: issue.project) }
let(:scoped_issue) { Issue.includes(notes: :author).find(issue.id) }
it 'indicates if the notes have their authors loaded' do
expect(issue.notes).not_to be_authors_loaded
expect(scoped_issue.notes).to be_authors_loaded
end
end
end end
describe 'Included modules' do describe 'Included modules' do
...@@ -245,6 +255,22 @@ describe Issue, "Issuable" do ...@@ -245,6 +255,22 @@ describe Issue, "Issuable" do
end end
end end
describe '#user_notes_count' do
let(:project) { create(:project) }
let(:issue1) { create(:issue, project: project) }
let(:issue2) { create(:issue, project: project) }
before do
create_list(:note, 3, noteable: issue1, project: project)
create_list(:note, 6, noteable: issue2, project: project)
end
it 'counts the user notes' do
expect(issue1.user_notes_count).to be(3)
expect(issue2.user_notes_count).to be(6)
end
end
describe "votes" do describe "votes" do
let(:project) { issue.project } let(:project) { issue.project }
......
...@@ -68,7 +68,10 @@ describe User, models: true do ...@@ -68,7 +68,10 @@ describe User, models: true do
describe 'email' do describe 'email' do
context 'when no signup domains listed' do context 'when no signup domains listed' do
before { allow_any_instance_of(ApplicationSetting).to receive(:restricted_signup_domains).and_return([]) } before do
allow_any_instance_of(ApplicationSetting).to receive(:restricted_signup_domains).and_return([])
end
it 'accepts any email' do it 'accepts any email' do
user = build(:user, email: "info@example.com") user = build(:user, email: "info@example.com")
expect(user).to be_valid expect(user).to be_valid
...@@ -76,7 +79,10 @@ describe User, models: true do ...@@ -76,7 +79,10 @@ describe User, models: true do
end end
context 'when a signup domain is listed and subdomains are allowed' do context 'when a signup domain is listed and subdomains are allowed' do
before { allow_any_instance_of(ApplicationSetting).to receive(:restricted_signup_domains).and_return(['example.com', '*.example.com']) } before do
allow_any_instance_of(ApplicationSetting).to receive(:restricted_signup_domains).and_return(['example.com', '*.example.com'])
end
it 'accepts info@example.com' do it 'accepts info@example.com' do
user = build(:user, email: "info@example.com") user = build(:user, email: "info@example.com")
expect(user).to be_valid expect(user).to be_valid
...@@ -94,7 +100,9 @@ describe User, models: true do ...@@ -94,7 +100,9 @@ describe User, models: true do
end end
context 'when a signup domain is listed and subdomains are not allowed' do context 'when a signup domain is listed and subdomains are not allowed' do
before { allow_any_instance_of(ApplicationSetting).to receive(:restricted_signup_domains).and_return(['example.com']) } before do
allow_any_instance_of(ApplicationSetting).to receive(:restricted_signup_domains).and_return(['example.com'])
end
it 'accepts info@example.com' do it 'accepts info@example.com' do
user = build(:user, email: "info@example.com") user = build(:user, email: "info@example.com")
...@@ -202,7 +210,10 @@ describe User, models: true do ...@@ -202,7 +210,10 @@ describe User, models: true do
end end
describe '#confirm' do describe '#confirm' do
before { allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(true) } before do
allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(true)
end
let(:user) { create(:user, confirmed_at: nil, unconfirmed_email: 'test@gitlab.com') } let(:user) { create(:user, confirmed_at: nil, unconfirmed_email: 'test@gitlab.com') }
it 'returns unconfirmed' do it 'returns unconfirmed' do
...@@ -845,6 +856,75 @@ describe User, models: true do ...@@ -845,6 +856,75 @@ describe User, models: true do
it { is_expected.to eq([private_project]) } it { is_expected.to eq([private_project]) }
end end
describe '#ci_authorized_runners' do
let(:user) { create(:user) }
let(:runner) { create(:ci_runner) }
before do
project.runners << runner
end
context 'without any projects' do
let(:project) { create(:project) }
it 'does not load' do
expect(user.ci_authorized_runners).to be_empty
end
end
context 'with personal projects runners' do
let(:namespace) { create(:namespace, owner: user) }
let(:project) { create(:project, namespace: namespace) }
it 'loads' do
expect(user.ci_authorized_runners).to contain_exactly(runner)
end
end
shared_examples :member do
context 'when the user is a master' do
before do
add_user(Gitlab::Access::MASTER)
end
it 'loads' do
expect(user.ci_authorized_runners).to contain_exactly(runner)
end
end
context 'when the user is a developer' do
before do
add_user(Gitlab::Access::DEVELOPER)
end
it 'does not load' do
expect(user.ci_authorized_runners).to be_empty
end
end
end
context 'with groups projects runners' do
let(:group) { create(:group) }
let(:project) { create(:project, group: group) }
def add_user(access)
group.add_user(user, access)
end
it_behaves_like :member
end
context 'with other projects runners' do
let(:project) { create(:project) }
def add_user(access)
project.team << [user, access]
end
it_behaves_like :member
end
end
describe '#viewable_starred_projects' do describe '#viewable_starred_projects' do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:public_project) { create(:empty_project, :public) } let(:public_project) { create(:empty_project, :public) }
......
require "spec_helper"
describe 'Git HTTP requests', lib: true do
let(:user) { create(:user) }
let(:project) { create(:project, path: 'project.git-project') }
it "gives WWW-Authenticate hints" do
clone_get('doesnt/exist.git')
expect(response.header['WWW-Authenticate']).to start_with('Basic ')
end
context "when the project doesn't exist" do
context "when no authentication is provided" do
it "responds with status 401 (no project existence information leak)" do
download('doesnt/exist.git') do |response|
expect(response.status).to eq(401)
end
end
end
context "when username and password are provided" do
context "when authentication fails" do
it "responds with status 401" do
download('doesnt/exist.git', user: user.username, password: "nope") do |response|
expect(response.status).to eq(401)
end
end
end
context "when authentication succeeds" do
it "responds with status 404" do
download('/doesnt/exist.git', user: user.username, password: user.password) do |response|
expect(response.status).to eq(404)
end
end
end
end
end
context "when the Wiki for a project exists" do
it "responds with the right project" do
wiki = ProjectWiki.new(project)
project.update_attribute(:visibility_level, Project::PUBLIC)
download("/#{wiki.repository.path_with_namespace}.git") do |response|
json_body = ActiveSupport::JSON.decode(response.body)
expect(response.status).to eq(200)
expect(json_body['RepoPath']).to include(wiki.repository.path_with_namespace)
end
end
end
context "when the project exists" do
let(:path) { "#{project.path_with_namespace}.git" }
context "when the project is public" do
before do
project.update_attribute(:visibility_level, Project::PUBLIC)
end
it "downloads get status 200" do
download(path, {}) do |response|
expect(response.status).to eq(200)
end
end
it "uploads get status 401" do
upload(path, {}) do |response|
expect(response.status).to eq(401)
end
end
context "with correct credentials" do
let(:env) { { user: user.username, password: user.password } }
it "uploads get status 200 (because Git hooks do the real check)" do
upload(path, env) do |response|
expect(response.status).to eq(200)
end
end
context 'but git-receive-pack is disabled' do
it "responds with status 404" do
allow(Gitlab.config.gitlab_shell).to receive(:receive_pack).and_return(false)
upload(path, env) do |response|
expect(response.status).to eq(404)
end
end
end
end
context 'but git-upload-pack is disabled' do
it "responds with status 404" do
allow(Gitlab.config.gitlab_shell).to receive(:upload_pack).and_return(false)
download(path, {}) do |response|
expect(response.status).to eq(404)
end
end
end
end
context "when the project is private" do
before do
project.update_attribute(:visibility_level, Project::PRIVATE)
end
context "when no authentication is provided" do
it "responds with status 401 to downloads" do
download(path, {}) do |response|
expect(response.status).to eq(401)
end
end
it "responds with status 401 to uploads" do
upload(path, {}) do |response|
expect(response.status).to eq(401)
end
end
end
context "when username and password are provided" do
let(:env) { { user: user.username, password: 'nope' } }
context "when authentication fails" do
it "responds with status 401" do
download(path, env) do |response|
expect(response.status).to eq(401)
end
end
context "when the user is IP banned" do
it "responds with status 401" do
expect(Rack::Attack::Allow2Ban).to receive(:filter).and_return(true)
allow_any_instance_of(Rack::Request).to receive(:ip).and_return('1.2.3.4')
clone_get(path, env)
expect(response.status).to eq(401)
end
end
end
context "when authentication succeeds" do
let(:env) { { user: user.username, password: user.password } }
context "when the user has access to the project" do
before do
project.team << [user, :master]
end
context "when the user is blocked" do
it "responds with status 404" do
user.block
project.team << [user, :master]
download(path, env) do |response|
expect(response.status).to eq(404)
end
end
end
context "when the user isn't blocked" do
it "downloads get status 200" do
expect(Rack::Attack::Allow2Ban).to receive(:reset)
clone_get(path, env)
expect(response.status).to eq(200)
end
it "uploads get status 200" do
upload(path, env) do |response|
expect(response.status).to eq(200)
end
end
end
context "when an oauth token is provided" do
before do
application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user)
@token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id)
end
it "downloads get status 200" do
clone_get "#{project.path_with_namespace}.git", user: 'oauth2', password: @token.token
expect(response.status).to eq(200)
end
it "uploads get status 401 (no project existence information leak)" do
push_get "#{project.path_with_namespace}.git", user: 'oauth2', password: @token.token
expect(response.status).to eq(401)
end
end
context "when blank password attempts follow a valid login" do
def attempt_login(include_password)
password = include_password ? user.password : ""
clone_get path, user: user.username, password: password
response.status
end
it "repeated attempts followed by successful attempt" do
options = Gitlab.config.rack_attack.git_basic_auth
maxretry = options[:maxretry] - 1
ip = '1.2.3.4'
allow_any_instance_of(Rack::Request).to receive(:ip).and_return(ip)
Rack::Attack::Allow2Ban.reset(ip, options)
maxretry.times.each do
expect(attempt_login(false)).to eq(401)
end
expect(attempt_login(true)).to eq(200)
expect(Rack::Attack::Allow2Ban.banned?(ip)).to be_falsey
maxretry.times.each do
expect(attempt_login(false)).to eq(401)
end
Rack::Attack::Allow2Ban.reset(ip, options)
end
end
end
context "when the user doesn't have access to the project" do
it "downloads get status 404" do
download(path, user: user.username, password: user.password) do |response|
expect(response.status).to eq(404)
end
end
it "uploads get status 200 (because Git hooks do the real check)" do
upload(path, user: user.username, password: user.password) do |response|
expect(response.status).to eq(200)
end
end
end
end
end
context "when a gitlab ci token is provided" do
let(:token) { 123 }
let(:project) { FactoryGirl.create :empty_project }
before do
project.update_attributes(runners_token: token, builds_enabled: true)
end
it "downloads get status 200" do
clone_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: token
expect(response.status).to eq(200)
end
it "uploads get status 401 (no project existence information leak)" do
push_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: token
expect(response.status).to eq(401)
end
end
end
end
context "when the project path doesn't end in .git" do
context "GET info/refs" do
let(:path) { "/#{project.path_with_namespace}/info/refs" }
context "when no params are added" do
before { get path }
it "redirects to the .git suffix version" do
expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs")
end
end
context "when the upload-pack service is requested" do
let(:params) { { service: 'git-upload-pack' } }
before { get path, params }
it "redirects to the .git suffix version" do
expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs?service=#{params[:service]}")
end
end
context "when the receive-pack service is requested" do
let(:params) { { service: 'git-receive-pack' } }
before { get path, params }
it "redirects to the .git suffix version" do
expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs?service=#{params[:service]}")
end
end
context "when the params are anything else" do
let(:params) { { service: 'git-implode-pack' } }
before { get path, params }
it "redirects to the sign-in page" do
expect(response).to redirect_to(new_user_session_path)
end
end
end
context "POST git-upload-pack" do
it "fails to find a route" do
expect { clone_post(project.path_with_namespace) }.to raise_error(ActionController::RoutingError)
end
end
context "POST git-receive-pack" do
it "failes to find a route" do
expect { push_post(project.path_with_namespace) }.to raise_error(ActionController::RoutingError)
end
end
end
context "retrieving an info/refs file" do
before { project.update_attribute(:visibility_level, Project::PUBLIC) }
context "when the file exists" do
before do
# Provide a dummy file in its place
allow_any_instance_of(Repository).to receive(:blob_at).and_call_original
allow_any_instance_of(Repository).to receive(:blob_at).with('5937ac0a7beb003549fc5fd26fc247adbce4a52e', 'info/refs') do
Gitlab::Git::Blob.find(project.repository, 'master', '.gitignore')
end
get "/#{project.path_with_namespace}/blob/master/info/refs"
end
it "returns the file" do
expect(response.status).to eq(200)
end
end
context "when the file exists" do
before { get "/#{project.path_with_namespace}/blob/master/info/refs" }
it "returns not found" do
expect(response.status).to eq(404)
end
end
end
def clone_get(project, options={})
get "/#{project}/info/refs", { service: 'git-upload-pack' }, auth_env(*options.values_at(:user, :password))
end
def clone_post(project, options={})
post "/#{project}/git-upload-pack", {}, auth_env(*options.values_at(:user, :password))
end
def push_get(project, options={})
get "/#{project}/info/refs", { service: 'git-receive-pack' }, auth_env(*options.values_at(:user, :password))
end
def push_post(project, options={})
post "/#{project}/git-receive-pack", {}, auth_env(*options.values_at(:user, :password))
end
def download(project, user: nil, password: nil)
args = [project, { user: user, password: password }]
clone_get(*args)
yield response
clone_post(*args)
yield response
end
def upload(project, user: nil, password: nil)
args = [project, { user: user, password: password }]
push_get(*args)
yield response
push_post(*args)
yield response
end
def auth_env(user, password)
if user && password
{ 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials(user, password) }
else
{}
end
end
end
...@@ -44,7 +44,7 @@ describe JwtController do ...@@ -44,7 +44,7 @@ describe JwtController do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:headers) { { authorization: credentials('user', 'password') } } let(:headers) { { authorization: credentials('user', 'password') } }
before { expect_any_instance_of(Gitlab::Auth).to receive(:find).with('user', 'password').and_return(user) } before { expect(Gitlab::Auth).to receive(:find_in_gitlab_or_ldap).with('user', 'password').and_return(user) }
subject! { get '/jwt/auth', parameters, headers } subject! { get '/jwt/auth', parameters, headers }
......
...@@ -124,7 +124,7 @@ describe Projects::ImportService, services: true do ...@@ -124,7 +124,7 @@ describe Projects::ImportService, services: true do
} }
) )
Gitlab.config.omniauth.providers << provider allow(Gitlab.config.omniauth).to receive(:providers).and_return([provider])
end end
end end
end end
...@@ -18,7 +18,7 @@ describe TodoService, services: true do ...@@ -18,7 +18,7 @@ describe TodoService, services: true do
end end
describe 'Issues' do describe 'Issues' do
let(:issue) { create(:issue, project: project, assignee: john_doe, author: author, description: mentions) } let(:issue) { create(:issue, project: project, assignee: john_doe, author: author, description: "- [ ] Task 1\n- [ ] Task 2 #{mentions}") }
let(:unassigned_issue) { create(:issue, project: project, assignee: nil) } let(:unassigned_issue) { create(:issue, project: project, assignee: nil) }
let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee, description: mentions) } let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee, description: mentions) }
...@@ -101,6 +101,19 @@ describe TodoService, services: true do ...@@ -101,6 +101,19 @@ describe TodoService, services: true do
should_create_todo(user: admin, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) should_create_todo(user: admin, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
should_not_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) should_not_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
end end
it 'does not create todo when when tasks are marked as completed' do
issue.update(description: "- [x] Task 1\n- [X] Task 2 #{mentions}")
service.update_issue(issue, author)
should_not_create_todo(user: admin, target: issue, action: Todo::MENTIONED)
should_not_create_todo(user: assignee, target: issue, action: Todo::MENTIONED)
should_not_create_todo(user: author, target: issue, action: Todo::MENTIONED)
should_not_create_todo(user: john_doe, target: issue, action: Todo::MENTIONED)
should_not_create_todo(user: member, target: issue, action: Todo::MENTIONED)
should_not_create_todo(user: non_member, target: issue, action: Todo::MENTIONED)
end
end end
describe '#close_issue' do describe '#close_issue' do
...@@ -210,7 +223,7 @@ describe TodoService, services: true do ...@@ -210,7 +223,7 @@ describe TodoService, services: true do
end end
describe 'Merge Requests' do describe 'Merge Requests' do
let(:mr_assigned) { create(:merge_request, source_project: project, author: author, assignee: john_doe, description: mentions) } let(:mr_assigned) { create(:merge_request, source_project: project, author: author, assignee: john_doe, description: "- [ ] Task 1\n- [ ] Task 2 #{mentions}") }
let(:mr_unassigned) { create(:merge_request, source_project: project, author: author, assignee: nil) } let(:mr_unassigned) { create(:merge_request, source_project: project, author: author, assignee: nil) }
describe '#new_merge_request' do describe '#new_merge_request' do
...@@ -253,6 +266,19 @@ describe TodoService, services: true do ...@@ -253,6 +266,19 @@ describe TodoService, services: true do
expect { service.update_merge_request(mr_assigned, author) }.not_to change(member.todos, :count) expect { service.update_merge_request(mr_assigned, author) }.not_to change(member.todos, :count)
end end
it 'does not create todo when when tasks are marked as completed' do
mr_assigned.update(description: "- [x] Task 1\n- [X] Task 2 #{mentions}")
service.update_merge_request(mr_assigned, author)
should_not_create_todo(user: admin, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: assignee, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: member, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED)
end
end end
describe '#close_merge_request' do describe '#close_merge_request' do
......
...@@ -28,6 +28,6 @@ module ImportSpecHelper ...@@ -28,6 +28,6 @@ module ImportSpecHelper
app_id: 'asd123', app_id: 'asd123',
app_secret: 'asd123' app_secret: 'asd123'
) )
Gitlab.config.omniauth.providers << provider allow(Gitlab.config.omniauth).to receive(:providers).and_return([provider])
end end
end end
...@@ -32,6 +32,10 @@ class MarkdownFeature ...@@ -32,6 +32,10 @@ class MarkdownFeature
@project_wiki ||= ProjectWiki.new(project, user) @project_wiki ||= ProjectWiki.new(project, user)
end end
def project_wiki_page
@project_wiki_page ||= build(:wiki_page, wiki: project_wiki)
end
def issue def issue
@issue ||= create(:issue, project: project) @issue ||= create(:issue, project: project)
end end
......
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