Commit 96e6cc9c authored by Valery Sizov's avatar Valery Sizov

Merge branch 'ce_upstream' into 'master'

CE upstream



See merge request !235
parents c8bbbdc7 9a7e3061
...@@ -23,6 +23,7 @@ config/gitlab.yml ...@@ -23,6 +23,7 @@ config/gitlab.yml
config/gitlab_ci.yml config/gitlab_ci.yml
config/initializers/rack_attack.rb config/initializers/rack_attack.rb
config/initializers/smtp_settings.rb config/initializers/smtp_settings.rb
config/initializers/relative_url.rb
config/resque.yml config/resque.yml
config/unicorn.rb config/unicorn.rb
config/secrets.yml config/secrets.yml
......
...@@ -5,12 +5,32 @@ v 8.6.0 (unreleased) ...@@ -5,12 +5,32 @@ v 8.6.0 (unreleased)
- Improve the formatting for the user page bio (Connor Shea) - Improve the formatting for the user page bio (Connor Shea)
- Fix issue when pushing to projects ending in .wiki - Fix issue when pushing to projects ending in .wiki
- Fix avatar stretching by providing a cropping feature (Johann Pardanaud) - Fix avatar stretching by providing a cropping feature (Johann Pardanaud)
- Don't load all of GitLab in mail_room
- Strip leading and trailing spaces in URL validator (evuez) - Strip leading and trailing spaces in URL validator (evuez)
- Return empty array instead of 404 when commit has no statuses in commit status API
- Update documentation to reflect Guest role not being enforced on internal projects - Update documentation to reflect Guest role not being enforced on internal projects
- Allow search for logged out users
- Don't show Issues/MRs from archived projects in Groups view
v 8.5.3
- Flush repository caches before renaming projects
v 8.5.2 v 8.5.2
- Fix sidebar overlapping content when screen width was below 1200px - Fix sidebar overlapping content when screen width was below 1200px
- Don't repeat labels listed on Labels tab
- Bring the "branded appearance" feature from EE to CE
- Fix error 500 when commenting on a commit - Fix error 500 when commenting on a commit
- Show days remaining instead of elapsed time for Milestone
- Fix broken icons on installations with relative URL (Artem Sidorenko)
- Fix issue where tag list wasn't refreshed after deleting a tag
- Fix import from gitlab.com (KazSawada)
- Improve implementation to check read access to forks and add pagination
- Don't show any "2FA required" message if it's not actually required
- Fix help keyboard shortcut on relative URL setups (Artem Sidorenko)
- Update Rails to 4.2.5.2
- Fix permissions for deprecated CI build status badge
- Don't show "Welcome to GitLab" when the search didn't return any projects
- Add Todos documentation
v 8.5.1 v 8.5.1
- Fix group projects styles - Fix group projects styles
......
...@@ -8,6 +8,9 @@ ...@@ -8,6 +8,9 @@
- [Closing policy for issues and merge requests](#closing-policy-for-issues-and-merge-requests) - [Closing policy for issues and merge requests](#closing-policy-for-issues-and-merge-requests)
- [Helping others](#helping-others) - [Helping others](#helping-others)
- [I want to contribute!](#i-want-to-contribute) - [I want to contribute!](#i-want-to-contribute)
- [Implement design & UI elements](#implement-design-ui-elements)
- [Design reference](#design-reference)
- [UI development kit](#ui-development-kit)
- [Issue tracker](#issue-tracker) - [Issue tracker](#issue-tracker)
- [Feature proposals](#feature-proposals) - [Feature proposals](#feature-proposals)
- [Issue tracker guidelines](#issue-tracker-guidelines) - [Issue tracker guidelines](#issue-tracker-guidelines)
...@@ -83,6 +86,22 @@ GitLab. ...@@ -83,6 +86,22 @@ GitLab.
This was inspired by [an article by Kent C. Dodds][medium-up-for-grabs]. This was inspired by [an article by Kent C. Dodds][medium-up-for-grabs].
## Implement design & UI elements
### Design reference
The GitLab design reference can be found in the [gitlab-design] project.
The designs are made using Antetype (`.atype` files). You can use the
[free Antetype viewer (Mac OSX only)] or grab an exported PNG from the design
(the PNG is 1:1).
The current designs can be found in the [`gitlab1.atype` file].
### UI development kit
Implemented UI elements can also be found at https://gitlab.com/help/ui. Please
note that this page isn't comprehensive at this time.
## Issue tracker ## Issue tracker
To get support for your particular problem please use the To get support for your particular problem please use the
...@@ -299,13 +318,14 @@ to us than having a minimal commit log. The smaller an MR is the more likely it ...@@ -299,13 +318,14 @@ to us than having a minimal commit log. The smaller an MR is the more likely it
is it will be merged (quickly). After that you can send more MRs to enhance it. is it will be merged (quickly). After that you can send more MRs to enhance it.
For examples of feedback on merge requests please look at already For examples of feedback on merge requests please look at already
[closed merge requests][closed-merge-requests]. If you would like quick feedback on your merge [closed merge requests][closed-merge-requests]. If you would like quick feedback
request feel free to mention one of the Merge Marshalls of the [core team][core-team]. on your merge request feel free to mention one of the Merge Marshalls in the
[core team][core-team] or one of the
[Merge request coaches](https://about.gitlab.com/team/).
Please ensure that your merge request meets the contribution acceptance criteria. Please ensure that your merge request meets the contribution acceptance criteria.
When having your code reviewed and when reviewing merge requests please take the When having your code reviewed and when reviewing merge requests please take the
[thoughtbot code review guidelines](https://github.com/thoughtbot/guides/tree/master/code-review) [Thoughtbot code review guide] into account.
into account.
### Merge request description format ### Merge request description format
...@@ -473,3 +493,7 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor ...@@ -473,3 +493,7 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
[rss-source]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#source-code-layout [rss-source]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#source-code-layout
[rss-naming]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#naming [rss-naming]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#naming
[doc-styleguide]: doc/development/doc_styleguide.md "Documentation styleguide" [doc-styleguide]: doc/development/doc_styleguide.md "Documentation styleguide"
[gitlab-design]: https://gitlab.com/gitlab-org/gitlab-design
[free Antetype viewer (Mac OSX only)]: https://itunes.apple.com/us/app/antetype-viewer/id824152298?mt=12
[`gitlab1.atype` file]: https://gitlab.com/gitlab-org/gitlab-design/tree/master/gitlab1.atype/
[Thoughtbot code review guide]: https://github.com/thoughtbot/guides/tree/master/code-review
...@@ -6,9 +6,9 @@ gem 'rails-deprecated_sanitizer', '~> 1.0.3' ...@@ -6,9 +6,9 @@ gem 'rails-deprecated_sanitizer', '~> 1.0.3'
# Responders respond_to and respond_with # Responders respond_to and respond_with
gem 'responders', '~> 2.0' gem 'responders', '~> 2.0'
# Specify a sprockets version due to security issue # Specify a sprockets version due to increased performance
# See https://groups.google.com/forum/#!topic/rubyonrails-security/doAVp0YaTqY # See https://gitlab.com/gitlab-org/gitlab-ce/issues/6069
gem 'sprockets', '~> 2.12.3' gem 'sprockets', '~> 3.3.5'
# Default values for AR models # Default values for AR models
gem "default_value_for", "~> 3.0.0" gem "default_value_for", "~> 3.0.0"
......
...@@ -431,7 +431,6 @@ GEM ...@@ -431,7 +431,6 @@ GEM
railties (>= 4.0.1) railties (>= 4.0.1)
hashie (3.4.3) hashie (3.4.3)
highline (1.7.8) highline (1.7.8)
hike (1.2.3)
hipchat (1.5.2) hipchat (1.5.2)
httparty httparty
mimemagic mimemagic
...@@ -795,11 +794,8 @@ GEM ...@@ -795,11 +794,8 @@ GEM
spring (>= 0.9.1) spring (>= 0.9.1)
spring-commands-teaspoon (0.0.2) spring-commands-teaspoon (0.0.2)
spring (>= 0.9.1) spring (>= 0.9.1)
sprockets (2.12.4) sprockets (3.3.5)
hike (~> 1.2) rack (> 1, < 3)
multi_json (~> 1.0)
rack (~> 1.0)
tilt (~> 1.1, != 1.3.0)
sprockets-rails (2.3.3) sprockets-rails (2.3.3)
actionpack (>= 3.0) actionpack (>= 3.0)
activesupport (>= 3.0) activesupport (>= 3.0)
...@@ -831,7 +827,7 @@ GEM ...@@ -831,7 +827,7 @@ GEM
rack (~> 1.0) rack (~> 1.0)
thor (0.19.1) thor (0.19.1)
thread_safe (0.3.5) thread_safe (0.3.5)
tilt (1.4.1) tilt (2.0.2)
timfel-krb5-auth (0.8.3) timfel-krb5-auth (0.8.3)
tinder (1.10.1) tinder (1.10.1)
eventmachine (~> 1.0) eventmachine (~> 1.0)
...@@ -1057,7 +1053,7 @@ DEPENDENCIES ...@@ -1057,7 +1053,7 @@ DEPENDENCIES
spring-commands-rspec (~> 1.0.4) spring-commands-rspec (~> 1.0.4)
spring-commands-spinach (~> 1.0.0) spring-commands-spinach (~> 1.0.0)
spring-commands-teaspoon (~> 0.0.2) spring-commands-teaspoon (~> 0.0.2)
sprockets (~> 2.12.3) sprockets (~> 3.3.5)
state_machines-activerecord (~> 0.3.0) state_machines-activerecord (~> 0.3.0)
task_list (~> 1.0.2) task_list (~> 1.0.2)
teaspoon (~> 1.0.0) teaspoon (~> 1.0.0)
......
...@@ -2,23 +2,39 @@ ...@@ -2,23 +2,39 @@
## Purpose of describing the contributing process ## Purpose of describing the contributing process
Below we describe the contributing process to GitLab for two reasons. So that contributors know what to expect from maintainers (possible responses, friendly treatment, etc.). And so that maintainers know what to expect from contributors (use the latest version, ensure that the issue is addressed, friendly treatment, etc.). Below we describe the contributing process to GitLab for two reasons. So that
contributors know what to expect from maintainers (possible responses, friendly
treatment, etc.). And so that maintainers know what to expect from contributors
(use the latest version, ensure that the issue is addressed, friendly treatment,
etc.).
## Common actions ## Common actions
### Issue team ### Issue team
- Looks for issues without [workflow labels](#how-we-handle-issues) and triages issue
- Closes invalid issues with a comment (duplicates, [fixed in newer version](#issue-fixed-in-newer-version), [issue report for old version](#issue-report-for-old-version), not a problem in GitLab, etc.) - Looks for issues without [workflow labels](#how-we-handle-issues) and triages
- Asks for feedback from issue reporter ([invalid issue reports](#improperly-formatted-issue), [format code](#code-format), etc.) issue
- Monitors all issues for feedback (but especially ones commented on since automatically watching them) - Closes invalid issues with a comment (duplicates,
[fixed in newer version](#issue-fixed-in-newer-version),
[issue report for old version](#issue-report-for-old-version), not a problem
in GitLab, etc.)
- Asks for feedback from issue reporter
([invalid issue reports](#improperly-formatted-issue),
[format code](#code-format), etc.)
- Monitors all issues for feedback (but especially ones commented on since
automatically watching them)
- Closes issues with no feedback from the reporter for two weeks - Closes issues with no feedback from the reporter for two weeks
### Merge marshal ### Merge marshall & merge request coach
- Responds to merge requests the issue team mentions them in and monitors for new merge requests - Responds to merge requests the issue team mentions them in and monitors for
- Provides feedback to the merge request submitter to improve the merge request (style, tests, etc.) new merge requests
- Mark merge requests 'ready-for-merge' when they meet the contribution guidelines - Provides feedback to the merge request submitter to improve the merge request
- Mention developer(s) based on the [list of members and their specialities](https://about.gitlab.com/core-team/) (style, tests, etc.)
- Mark merge requests `Ready for Merge` when they meet the
[contribution acceptance criteria]
- Mention developer(s) based on the
[list of members and their specialities][team]
- Closes merge requests with no feedback from the reporter for two weeks - Closes merge requests with no feedback from the reporter for two weeks
## Priorities of the issue team ## Priorities of the issue team
...@@ -30,29 +46,40 @@ Below we describe the contributing process to GitLab for two reasons. So that co ...@@ -30,29 +46,40 @@ Below we describe the contributing process to GitLab for two reasons. So that co
## Mentioning people ## Mentioning people
The most important thing is making sure valid issues receive feedback from the development team. Therefore the priority is mentioning developers that can help on those issue. Please select someone with relevant experience from [GitLab core team](https://about.gitlab.com/core-team/). If there is nobody mentioned with that expertise look in the commit history for the affected files to find someone. Avoid mentioning the lead developer, this is the person that is least likely to give a timely response. If the involvement of the lead developer is needed the other core team members will mention this person. The most important thing is making sure valid issues receive feedback from the
development team. Therefore the priority is mentioning developers that can help
on those issue. Please select someone with relevant experience from
[GitLab core team][core-team]. If there is nobody mentioned with that expertise
look in the commit history for the affected files to find someone. Avoid
mentioning the lead developer, this is the person that is least likely to give a
timely response. If the involvement of the lead developer is needed the other
core team members will mention this person.
## Workflow labels ## Workflow labels
Workflow labels are purposely not very detailed since that would be hard to keep updated as you would need to re-evaluate them after every comment. We optionally use functional labels on demand when want to group related issues to get an overview (for example all issues related to RVM, to tackle them in one go) and to add details to the issue. Workflow labels are purposely not very detailed since that would be hard to keep
updated as you would need to re-evaluate them after every comment. We optionally
- *Awaiting feedback*: Feedback pending from the reporter use functional labels on demand when want to group related issues to get an
- *Awaiting confirmation of fix*: The issue should already be solved in **master** (generally you can avoid this workflow item and just close the issue right away) overview (for example all issues related to RVM, to tackle them in one go) and
- *Attached MR*: There is a MR attached and the discussion should happen there to add details to the issue.
- We need to let issues stay in sync with the MR's. We can do this with a "Closing #XXXX" or "Fixes #XXXX" comment in the MR. We can't close the issue when there is a merge request because sometimes a MR is not good and we just close the MR, then the issue must stay.
- *Developer*: needs help from a developer - ~"Awaiting Feedback" Feedback pending from the reporter
- *UX* needs needs help from a UX designer - ~UX needs help from a UX designer
- *Frontend* needs help from a Front-end engineer - ~Frontend needs help from a Front-end engineer. Please follow the
- *Graphics* needs help from a Graphics designer ["Implement design & UI elements" guidelines].
- *up-for-grabs* is an issue suitable for first-time contributors, of reasonable difficulty and size. Not exclusive with other labels. - ~up-for-grabs is an issue suitable for first-time contributors, of reasonable difficulty and size. Not exclusive with other labels.
- *feature proposal* is a proposal for a new feature for GitLab. People are encouraged to vote - ~"feature proposal" is a proposal for a new feature for GitLab. People are encouraged to vote
in support or comment for further detail. Do not use `feature request`. in support or comment for further detail. Do not use `feature request`.
- ~bug is an issue reporting undesirable or incorrect behavior.
- ~customer is an issue reported by enterprise subscribers. This label should
be accompanied by *bug* or *feature proposal* labels.
Example workflow: when a UX designer provided a design but it needs frontend work they remove the UX label and add the frontend label. Example workflow: when a UX designer provided a design but it needs frontend work they remove the UX label and add the frontend label.
## Functional labels ## Functional labels
These labels describe what development specialities are involved such as: PostgreSQL, UX, LDAP. These labels describe what development specialities are involved such as: `CI`,
`Core`, `Documentation`, `Frontend`, `Issues`, `Merge Requests`, `Omnibus`,
`Release`, `Repository`, `UX`.
## Assigning issues ## Assigning issues
...@@ -60,21 +87,29 @@ If an issue is complex and needs the attention of a specific person, assignment ...@@ -60,21 +87,29 @@ If an issue is complex and needs the attention of a specific person, assignment
## Label colors ## Label colors
- Light orange `#fef2c0`: workflow labels for issue team members (awaiting feedback, awaiting confirmation of fix) - Light orange `#fef2c0`: workflow labels for issue team members (awaiting
- Bright orange `#eb6420`: workflow labels for core team members (attached MR, awaiting developer action/feedback) feedback, awaiting confirmation of fix)
- Bright orange `#eb6420`: workflow labels for core team members (attached MR,
awaiting developer action/feedback)
- Light blue `#82C5FF`: functional labels - Light blue `#82C5FF`: functional labels
- Green labels `#009800`: issues that can generally be ignored. For example, issues given the following labels normally can be closed immediately: - Green labels `#009800`: issues that can generally be ignored. For example,
- Support (see copy & paste response: [Support requests and configuration questions](#support-requests-and-configuration-questions) issues given the following labels normally can be closed immediately:
- Support (see copy & paste response:
[Support requests and configuration questions](#support-requests-and-configuration-questions)
## Be kind ## Be kind
Be kind to people trying to contribute. Be aware that people may be a non-native English speaker, they might not understand things or they might be very sensitive as to how you word things. Use Emoji to express your feelings (heart, star, smile, etc.). Some good tips about giving feedback to merge requests is in the [Thoughtbot code review guide](https://github.com/thoughtbot/guides/tree/master/code-review). Be kind to people trying to contribute. Be aware that people may be a non-native
English speaker, they might not understand things or they might be very
sensitive as to how you word things. Use Emoji to express your feelings (heart,
star, smile, etc.). Some good tips about giving feedback to merge requests is in
the [Thoughtbot code review guide].
## Copy & paste responses ## Copy & paste responses
### Improperly formatted issue ### Improperly formatted issue
Thanks for the issue report. Please reformat your issue to conform to the issue tracker guidelines found in our \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines). Thanks for the issue report. Please reformat your issue to conform to the \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines).
### Issue report for old version ### Issue report for old version
...@@ -110,11 +145,11 @@ This merge request has been closed because a request for more information has no ...@@ -110,11 +145,11 @@ This merge request has been closed because a request for more information has no
### Accepting merge requests ### Accepting merge requests
Is there an issue on the [issue tracker](https://gitlab.com/gitlab-org/gitlab-ce/issues) Is there an issue on the
that is similar to this? \[issue tracker\]\(https://gitlab.com/gitlab-org/gitlab-ce/issues) that is
Could you please link it here? similar to this? Could you please link it here?
Please be aware that new functionality that is not marked Please be aware that new functionality that is not marked
[accepting merge requests](https://gitlab.com/gitlab-org/gitlab-ce/issues?milestone_id=&scope=all&sort=created_desc&state=opened&utf8=%E2%9C%93&assignee_id=&author_id=&milestone_title=&label_name=Accepting+Merge+Requests) \[accepting merge requests\]\(https://gitlab.com/gitlab-org/gitlab-ce/issues?milestone_id=&scope=all&sort=created_desc&state=opened&utf8=%E2%9C%93&assignee_id=&author_id=&milestone_title=&label_name=Accepting+Merge+Requests)
might not make it into GitLab. might not make it into GitLab.
### Only accepting merge requests with green tests ### Only accepting merge requests with green tests
...@@ -129,4 +164,10 @@ rebase with master to see if that solves the issue. ...@@ -129,4 +164,10 @@ rebase with master to see if that solves the issue.
We are currently in the process of closing down the issue tracker on GitHub, to We are currently in the process of closing down the issue tracker on GitHub, to
prevent duplication with the GitLab.com issue tracker. prevent duplication with the GitLab.com issue tracker.
Since this is an older issue I'll be closing this for now. If you think this is Since this is an older issue I'll be closing this for now. If you think this is
still an issue I encourage you to open it on the \[GitLab.com issue tracker\](https://gitlab.com/gitlab-org/gitlab-ce/issues). still an issue I encourage you to open it on the \[GitLab.com issue tracker\]\(https://gitlab.com/gitlab-org/gitlab-ce/issues).
[core-team]: https://about.gitlab.com/core-team/
[team]: https://about.gitlab.com/team/
[contribution acceptance criteria]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#contribution-acceptance-criteria
["Implement design & UI elements" guidelines]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#implement-design-ui-elements
[Thoughtbot code review guide]: https://github.com/thoughtbot/guides/tree/master/code-review
...@@ -4,4 +4,7 @@ ...@@ -4,4 +4,7 @@
require File.expand_path('../config/application', __FILE__) require File.expand_path('../config/application', __FILE__)
relative_url_conf = File.expand_path('../config/initializers/relative_url', __FILE__)
require relative_url_conf if File.exist?("#{relative_url_conf}.rb")
Gitlab::Application.load_tasks Gitlab::Application.load_tasks
# Quick Submit behavior # Quick Submit behavior
# #
# When an input field with the `js-quick-submit` class receives a "Meta+Enter" # When a child field of a form with a `js-quick-submit` class receives a
# (Mac) or "Ctrl+Enter" (Linux/Windows) key combination, its parent form is # "Meta+Enter" (Mac) or "Ctrl+Enter" (Linux/Windows) key combination, the form
# submitted. # is submitted.
# #
#= require extensions/jquery #= require extensions/jquery
# #
# ### Example Markup # ### Example Markup
# #
# <form action="/foo"> # <form action="/foo" class="js-quick-submit">
# <input type="text" class="js-quick-submit" /> # <input type="text" />
# <textarea class="js-quick-submit"></textarea> # <textarea></textarea>
# <input type="submit" value="Submit" />
# </form> # </form>
# #
isMac = ->
navigator.userAgent.match(/Macintosh/)
keyCodeIs = (e, keyCode) ->
return false if (e.originalEvent && e.originalEvent.repeat) || e.repeat
return e.keyCode == keyCode
$(document).on 'keydown.quick_submit', '.js-quick-submit', (e) -> $(document).on 'keydown.quick_submit', '.js-quick-submit', (e) ->
return if (e.originalEvent && e.originalEvent.repeat) || e.repeat return unless keyCodeIs(e, 13) # Enter
return unless e.keyCode == 13 # Enter
if navigator.userAgent.match(/Macintosh/) if isMac()
return unless (e.metaKey && !e.altKey && !e.ctrlKey && !e.shiftKey) return unless (e.metaKey && !e.altKey && !e.ctrlKey && !e.shiftKey)
else else
return unless (e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey) return unless (e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey)
...@@ -27,3 +34,22 @@ $(document).on 'keydown.quick_submit', '.js-quick-submit', (e) -> ...@@ -27,3 +34,22 @@ $(document).on 'keydown.quick_submit', '.js-quick-submit', (e) ->
$form = $(e.target).closest('form') $form = $(e.target).closest('form')
$form.find('input[type=submit], button[type=submit]').disable() $form.find('input[type=submit], button[type=submit]').disable()
$form.submit() $form.submit()
# If the user tabs to a submit button on a `js-quick-submit` form, display a
# tooltip to let them know they could've used the hotkey
$(document).on 'keyup.quick_submit', '.js-quick-submit input[type=submit], .js-quick-submit button[type=submit]', (e) ->
return unless keyCodeIs(e, 9) # Tab
if isMac()
title = "You can also press &#8984;-Enter"
else
title = "You can also press Ctrl-Enter"
$this = $(@)
$this.tooltip(
container: 'body'
html: 'true'
placement: 'auto top'
title: title
trigger: 'manual'
).tooltip('show').one('blur', -> $this.tooltip('hide'))
@Dashboard =
init: ->
$(".projects-list-filter").off('keyup')
this.initSearch()
initSearch: ->
@timer = null
$(".projects-list-filter").on('keyup', ->
clearTimeout(@timer)
@timer = setTimeout(Dashboard.filterResults, 500)
)
filterResults: =>
$('.projects-list-holder').fadeTo(250, 0.5)
form = null
form = $("form#project-filter-form")
search = $(".projects-list-filter").val()
project_filter_url = form.attr('action') + '?' + form.serialize()
$.ajax
type: "GET"
url: form.attr('action')
data: form.serialize()
complete: ->
$('.projects-list-holder').fadeTo(250, 1)
success: (data) ->
$('.projects-list-holder').replaceWith(data.html)
# Change url so if user reload a page - search results are saved
history.replaceState {page: project_filter_url}, document.title, project_filter_url
dataType: "json"
...@@ -16,8 +16,6 @@ class Dispatcher ...@@ -16,8 +16,6 @@ class Dispatcher
shortcut_handler = null shortcut_handler = null
switch page switch page
when 'explore:projects:index', 'explore:projects:starred', 'explore:projects:trending'
Dashboard.init()
when 'projects:issues:index' when 'projects:issues:index'
Issues.init() Issues.init()
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsNavigation()
...@@ -59,8 +57,6 @@ class Dispatcher ...@@ -59,8 +57,6 @@ class Dispatcher
when 'projects:merge_requests:index' when 'projects:merge_requests:index'
shortcut_handler = new ShortcutsNavigation() shortcut_handler = new ShortcutsNavigation()
MergeRequests.init() MergeRequests.init()
when 'dashboard:show', 'root:show'
Dashboard.init()
when 'dashboard:activity' when 'dashboard:activity'
new Activities() new Activities()
when 'dashboard:projects:starred' when 'dashboard:projects:starred'
...@@ -107,9 +103,6 @@ class Dispatcher ...@@ -107,9 +103,6 @@ class Dispatcher
new ProjectFork() new ProjectFork()
when 'projects:artifacts:browse' when 'projects:artifacts:browse'
new BuildArtifacts() new BuildArtifacts()
when 'users:show'
new User()
new Activities()
when 'projects:group_links:index' when 'projects:group_links:index'
new GroupsSelect() new GroupsSelect()
when 'projects:mirrors:show', 'projects:mirrors:update' when 'projects:mirrors:show', 'projects:mirrors:update'
......
@Pager = @Pager =
init: (@limit = 0, preload, @disable = false) -> init: (@limit = 0, preload, @disable = false) ->
@loading = $(".loading") @loading = $('.loading').first()
if preload if preload
@offset = 0 @offset = 0
@getOld() @getOld()
......
...@@ -48,7 +48,7 @@ class @Profile ...@@ -48,7 +48,7 @@ class @Profile
$filename.text($filename.data('label')) $filename.text($filename.data('label'))
$('.js-upload-user-avatar').on 'click', -> $('.js-upload-user-avatar').on 'click', ->
$('.edit_user').submit() $('.edit-user').submit()
$avatarInput.on "change", -> $avatarInput.on "change", ->
form = $(this).closest("form") form = $(this).closest("form")
...@@ -62,4 +62,3 @@ class @Profile ...@@ -62,4 +62,3 @@ class @Profile
$modalCropImg.attr('src', event.target.result) $modalCropImg.attr('src', event.target.result)
fileData = reader.readAsDataURL(this.files[0]) fileData = reader.readAsDataURL(this.files[0])
class @ProjectsList @ProjectsList =
constructor: -> init: ->
$(".projects-list .js-expand").on 'click', (e) -> $(".projects-list-filter").off('keyup')
e.preventDefault() this.initSearch()
list = $(this).closest('.projects-list')
$("#filter_projects").on 'keyup', -> initSearch: ->
ProjectsList.filter_results($("#filter_projects")) @timer = null
$(".projects-list-filter").on('keyup', ->
clearTimeout(@timer)
@timer = setTimeout(ProjectsList.filterResults, 500)
)
@filter_results: ($element) -> filterResults: =>
terms = $element.val() $('.projects-list-holder').fadeTo(250, 0.5)
filterSelector = $element.data('filter-selector') || 'span.filter-title'
if not terms form = null
$(".projects-list li").show() form = $("form#project-filter-form")
$('.gl-pagination').show() search = $(".projects-list-filter").val()
else project_filter_url = form.attr('action') + '?' + form.serialize()
$(".projects-list li").each (index) ->
$this = $(this)
name = $this.find(filterSelector).text()
if name.toLowerCase().indexOf(terms.toLowerCase()) == -1 $.ajax
$this.hide() type: "GET"
else url: form.attr('action')
$this.show() data: form.serialize()
$('.gl-pagination').hide() complete: ->
$('.projects-list-holder').fadeTo(250, 1)
success: (data) ->
$('.projects-list-holder').replaceWith(data.html)
# Change url so if user reload a page - search results are saved
history.replaceState {page: project_filter_url}, document.title, project_filter_url
dataType: "json"
...@@ -13,8 +13,10 @@ class @Shortcuts ...@@ -13,8 +13,10 @@ class @Shortcuts
if $('#modal-shortcuts').length > 0 if $('#modal-shortcuts').length > 0
$('#modal-shortcuts').modal('show') $('#modal-shortcuts').modal('show')
else else
url = '/help/shortcuts'
url = gon.relative_url_root + url if gon.relative_url_root?
$.ajax( $.ajax(
url: '/help/shortcuts', url: url,
dataType: 'script', dataType: 'script',
success: (e) -> success: (e) ->
if location and location.length > 0 if location and location.length > 0
......
class @User class @User
constructor: -> constructor: (@opts) ->
$('.profile-groups-avatars').tooltip("placement": "top") $('.profile-groups-avatars').tooltip("placement": "top")
new ProjectsList()
@initTabs()
$('.hide-project-limit-message').on 'click', (e) -> $('.hide-project-limit-message').on 'click', (e) ->
path = '/' path = '/'
$.cookie('hide_project_limit_message', 'false', { path: path }) $.cookie('hide_project_limit_message', 'false', { path: path })
$(@).parents('.project-limit-message').remove() $(@).parents('.project-limit-message').remove()
e.preventDefault() e.preventDefault()
initTabs: ->
new UserTabs(
parentEl: '.user-profile'
action: @opts.action
)
# UserTabs
#
# Handles persisting and restoring the current tab selection and lazily-loading
# content on the Users#show page.
#
# ### Example Markup
#
# <ul class="nav-links">
# <li class="activity-tab active">
# <a data-action="activity" data-target="#activity" data-toggle="tab" href="/u/username">
# Activity
# </a>
# </li>
# <li class="groups-tab">
# <a data-action="groups" data-target="#groups" data-toggle="tab" href="/u/username/groups">
# Groups
# </a>
# </li>
# <li class="contributed-tab">
# <a data-action="contributed" data-target="#contributed" data-toggle="tab" href="/u/username/contributed">
# Contributed projects
# </a>
# </li>
# <li class="projects-tab">
# <a data-action="projects" data-target="#projects" data-toggle="tab" href="/u/username/projects">
# Personal projects
# </a>
# </li>
# </ul>
#
# <div class="tab-content">
# <div class="tab-pane" id="activity">
# Activity Content
# </div>
# <div class="tab-pane" id="groups">
# Groups Content
# </div>
# <div class="tab-pane" id="contributed">
# Contributed projects content
# </div>
# <div class="tab-pane" id="projects">
# Projects content
# </div>
# </div>
#
# <div class="loading-status">
# <div class="loading">
# Loading Animation
# </div>
# </div>
#
class @UserTabs
constructor: (opts) ->
{
@action = 'activity'
@defaultAction = 'activity'
@parentEl = $(document)
} = opts
# Make jQuery object if selector is provided
@parentEl = $(@parentEl) if typeof @parentEl is 'string'
# Store the `location` object, allowing for easier stubbing in tests
@_location = location
# Set tab states
@loaded = {}
for item in @parentEl.find('.nav-links a')
@loaded[$(item).attr 'data-action'] = false
# Actions
@actions = Object.keys @loaded
@bindEvents()
# Set active tab
@action = @defaultAction if @action is 'show'
@activateTab(@action)
bindEvents: ->
# Toggle event listeners
@parentEl
.off 'shown.bs.tab', '.nav-links a[data-toggle="tab"]'
.on 'shown.bs.tab', '.nav-links a[data-toggle="tab"]', @tabShown
tabShown: (event) =>
$target = $(event.target)
action = $target.data('action')
source = $target.attr('href')
@setTab(source, action)
@setCurrentAction(action)
activateTab: (action) ->
@parentEl.find(".nav-links .#{action}-tab a").tab('show')
setTab: (source, action) ->
return if @loaded[action] is true
if action is 'activity'
@loadActivities(source)
if action in ['groups', 'contributed', 'projects']
@loadTab(source, action)
loadTab: (source, action) ->
$.ajax
beforeSend: => @toggleLoading(true)
complete: => @toggleLoading(false)
dataType: 'json'
type: 'GET'
url: "#{source}.json"
success: (data) =>
tabSelector = 'div#' + action
@parentEl.find(tabSelector).html(data.html)
@loaded[action] = true
loadActivities: (source) ->
return if @loaded['activity'] is true
$calendarWrap = @parentEl.find('.user-calendar')
$calendarWrap.load($calendarWrap.data('href'))
new Activities()
@loaded['activity'] = true
toggleLoading: (status) ->
@parentEl.find('.loading-status .loading').toggle(status)
setCurrentAction: (action) ->
# Remove possible actions from URL
regExp = new RegExp('\/(' + @actions.join('|') + ')(\.html)?\/?$')
new_state = @_location.pathname
new_state = new_state.replace(/\/+$/, "") # remove trailing slashes
new_state = new_state.replace(regExp, '')
# Append the new action if we're on a tab other than 'activity'
unless action == @defaultAction
new_state += "/#{action}"
# Ensure parameters and hash come along for the ride
new_state += @_location.search + @_location.hash
history.replaceState {turbolinks: true, url: new_state}, document.title, new_state
new_state
...@@ -6,11 +6,15 @@ ...@@ -6,11 +6,15 @@
.cdark { color: #444 } .cdark { color: #444 }
/** COMMON CLASSES **/ /** COMMON CLASSES **/
.prepend-top-0 { margin-top: 0; }
.prepend-top-5 { margin-top: 5px; }
.prepend-top-10 { margin-top:10px } .prepend-top-10 { margin-top:10px }
.prepend-top-default { margin-top: $gl-padding !important; } .prepend-top-default { margin-top: $gl-padding !important; }
.prepend-top-20 { margin-top:20px } .prepend-top-20 { margin-top:20px }
.prepend-left-10 { margin-left:10px } .prepend-left-10 { margin-left:10px }
.prepend-left-default { margin-left:$gl-padding }
.prepend-left-20 { margin-left:20px } .prepend-left-20 { margin-left:20px }
.append-right-5 { margin-right: 5px }
.append-right-10 { margin-right:10px } .append-right-10 { margin-right:10px }
.append-right-20 { margin-right:20px } .append-right-20 { margin-right:20px }
.append-bottom-10 { margin-bottom:10px } .append-bottom-10 { margin-bottom:10px }
...@@ -314,7 +318,7 @@ table { ...@@ -314,7 +318,7 @@ table {
} }
.btn-sign-in { .btn-sign-in {
margin-top: 8px; margin-top: 10px;
text-shadow: none; text-shadow: none;
} }
......
...@@ -69,6 +69,10 @@ label { ...@@ -69,6 +69,10 @@ label {
&.inline-label { &.inline-label {
margin: 0; margin: 0;
} }
&.label-light {
font-weight: 600;
}
} }
.inline-input-group { .inline-input-group {
......
...@@ -79,6 +79,10 @@ ...@@ -79,6 +79,10 @@
> .dropdown { > .dropdown {
margin-right: $gl-padding-top; margin-right: $gl-padding-top;
display: inline-block; display: inline-block;
&:last-child {
margin-right: 0;
}
} }
> .btn { > .btn {
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
transition-duration: .3s; transition-duration: .3s;
} }
.home { .gitlab-text-container-link {
z-index: 1; z-index: 1;
position: absolute; position: absolute;
left: 0px; left: 0px;
......
...@@ -167,12 +167,6 @@ ...@@ -167,12 +167,6 @@
} }
} }
.alert-help {
background-color: $background-color;
border: 1px solid $border-color;
color: $gl-gray;
}
// Typography ================================================================= // Typography =================================================================
.text-primary, .text-primary,
......
...@@ -196,7 +196,7 @@ body { ...@@ -196,7 +196,7 @@ body {
h1, h2, h3, h4, h5, h6 { h1, h2, h3, h4, h5, h6 {
color: $gl-header-color; color: $gl-header-color;
font-weight: 500; font-weight: 600;
} }
/** CODE **/ /** CODE **/
......
...@@ -34,6 +34,7 @@ $error-exclamation-point: #E62958; ...@@ -34,6 +34,7 @@ $error-exclamation-point: #E62958;
$border-radius-default: 3px; $border-radius-default: 3px;
$list-title-color: #333333; $list-title-color: #333333;
$list-text-color: #555555; $list-text-color: #555555;
$profile-settings-link-color: $md-link-color;
/* /*
* Color schema * Color schema
......
...@@ -8,6 +8,10 @@ ...@@ -8,6 +8,10 @@
max-width: none; max-width: none;
} }
.flash-container {
margin-bottom: $gl-padding;
}
.brand-holder { .brand-holder {
font-size: 18px; font-size: 18px;
line-height: 1.5; line-height: 1.5;
......
...@@ -39,7 +39,7 @@ li.milestone { ...@@ -39,7 +39,7 @@ li.milestone {
margin-right: 10px; margin-right: 10px;
} }
.time-elapsed { .remaining-days {
color: $orange-light; color: $orange-light;
} }
} }
......
.global-notifications-form .level-title { .notification-list-item {
font-size: 15px; line-height: 34px;
color: #333;
font-weight: bold;
} }
.notification-icon-holder { .notification {
width: 20px; position: relative;
float: left; top: 1px;
> .fa {
font-size: 18px;
}
} }
.ns-part { .ns-part {
color: $gl-primary; color: $gl-text-green;
} }
.ns-watch { .ns-watch {
......
...@@ -5,12 +5,25 @@ ...@@ -5,12 +5,25 @@
} }
} }
.profile-avatar-form-option { .profile-settings-sidebar {
hr { a {
margin: 10px 0; color: $profile-settings-link-color;
} }
} }
.avatar-image {
@media (min-width: $screen-sm-min) {
float: left;
margin-bottom: 0;
}
}
.avatar-file-name {
position: relative;
top: 2px;
display: inline-block;
}
.oauth-buttons { .oauth-buttons {
.btn-group { .btn-group {
margin-right: 10px; margin-right: 10px;
...@@ -79,6 +92,13 @@ ...@@ -79,6 +92,13 @@
margin: auto; margin: auto;
} }
.user-avatar-button {
.file-name {
display: inline-block;
padding-left: 10px;
}
}
.modal-profile-crop { .modal-profile-crop {
.modal-dialog { .modal-dialog {
width: 500px; width: 500px;
......
.search-results { .search-results {
.search-result-row { .search-result-row {
border-bottom: 1px solid #DDD; border-bottom: 1px solid $border-color;
padding-bottom: 15px; padding-bottom: $gl-padding;
margin-bottom: 15px; margin-bottom: $gl-padding;
&:last-child {
border-bottom: none;
}
} }
} }
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
.badge.todos-pending-count { .badge.todos-pending-count {
background-color: #7f8fa4; background-color: #7f8fa4;
margin-top: -5px; margin-top: -5px;
font-weight: normal;
} }
} }
} }
......
...@@ -3,6 +3,7 @@ module Ci ...@@ -3,6 +3,7 @@ module Ci
before_action :project before_action :project
before_action :authorize_read_project!, except: [:badge] before_action :authorize_read_project!, except: [:badge]
before_action :no_cache, only: [:badge] before_action :no_cache, only: [:badge]
skip_before_action :authenticate_user!, only: [:badge]
protect_from_forgery protect_from_forgery
def show def show
...@@ -18,6 +19,7 @@ module Ci ...@@ -18,6 +19,7 @@ module Ci
# #
def badge def badge
return render_404 unless @project return render_404 unless @project
image = Ci::ImageForBuildService.new.execute(@project, params) image = Ci::ImageForBuildService.new.execute(@project, params)
send_file image.path, filename: image.name, disposition: 'inline', type:"image/svg+xml" send_file image.path, filename: image.name, disposition: 'inline', type:"image/svg+xml"
end end
......
...@@ -2,7 +2,7 @@ module IssuesAction ...@@ -2,7 +2,7 @@ module IssuesAction
extend ActiveSupport::Concern extend ActiveSupport::Concern
def issues def issues
@issues = get_issues_collection @issues = get_issues_collection.non_archived
@issues = @issues.page(params[:page]).per(ApplicationController::PER_PAGE) @issues = @issues.page(params[:page]).per(ApplicationController::PER_PAGE)
@issues = @issues.preload(:author, :project) @issues = @issues.preload(:author, :project)
......
...@@ -2,7 +2,7 @@ module MergeRequestsAction ...@@ -2,7 +2,7 @@ module MergeRequestsAction
extend ActiveSupport::Concern extend ActiveSupport::Concern
def merge_requests def merge_requests
@merge_requests = get_merge_requests_collection @merge_requests = get_merge_requests_collection.non_archived
@merge_requests = @merge_requests.page(params[:page]).per(ApplicationController::PER_PAGE) @merge_requests = @merge_requests.page(params[:page]).per(ApplicationController::PER_PAGE)
@merge_requests = @merge_requests.preload(:author, :target_project) @merge_requests = @merge_requests.preload(:author, :target_project)
......
...@@ -6,7 +6,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController ...@@ -6,7 +6,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
@projects = @projects.sort(@sort = params[:sort]) @projects = @projects.sort(@sort = params[:sort])
@projects = @projects.includes(:namespace) @projects = @projects.includes(:namespace)
terms = params['filter_projects'] terms = params[:filter_projects]
if terms.present? if terms.present?
@projects = @projects.search(terms) @projects = @projects.search(terms)
...@@ -35,7 +35,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController ...@@ -35,7 +35,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
@projects = @projects.includes(:namespace, :forked_from_project, :tags) @projects = @projects.includes(:namespace, :forked_from_project, :tags)
@projects = @projects.sort(@sort = params[:sort]) @projects = @projects.sort(@sort = params[:sort])
terms = params['filter_projects'] terms = params[:filter_projects]
if terms.present? if terms.present?
@projects = @projects.search(terms) @projects = @projects.search(terms)
......
...@@ -6,20 +6,24 @@ class Projects::ForksController < Projects::ApplicationController ...@@ -6,20 +6,24 @@ class Projects::ForksController < Projects::ApplicationController
def index def index
base_query = project.forks.includes(:creator) base_query = project.forks.includes(:creator)
@forks = if current_user @forks = base_query.merge(ProjectsFinder.new.execute(current_user))
base_query.where('projects.visibility_level IN (?) OR projects.id IN (?)',
Project.public_and_internal_levels,
current_user.authorized_projects.pluck(:id))
else
base_query.where('projects.visibility_level = ?', Project::PUBLIC)
end
@total_forks_count = base_query.size @total_forks_count = base_query.size
@private_forks_count = @total_forks_count - @forks.size @private_forks_count = @total_forks_count - @forks.size
@public_forks_count = @total_forks_count - @private_forks_count @public_forks_count = @total_forks_count - @private_forks_count
@sort = params[:sort] || 'id_desc' @sort = params[:sort] || 'id_desc'
@forks = @forks.search(params[:filter_projects]) if params[:filter_projects].present?
@forks = @forks.order_by(@sort).page(params[:page]).per(PER_PAGE) @forks = @forks.order_by(@sort).page(params[:page]).per(PER_PAGE)
respond_to do |format|
format.html
format.json do
render json: {
html: view_to_html_string("projects/forks/_projects", projects: @forks)
}
end
end
end end
def new def new
......
...@@ -34,6 +34,11 @@ class Projects::TagsController < Projects::ApplicationController ...@@ -34,6 +34,11 @@ class Projects::TagsController < Projects::ApplicationController
def destroy def destroy
DeleteTagService.new(project, current_user).execute(params[:id]) DeleteTagService.new(project, current_user).execute(params[:id])
respond_to do |format|
format.html do
redirect_to namespace_project_tags_path(@project.namespace, @project) redirect_to namespace_project_tags_path(@project.namespace, @project)
end end
format.js
end
end
end end
class SearchController < ApplicationController class SearchController < ApplicationController
skip_before_action :authenticate_user!, :reject_blocked
include SearchHelper include SearchHelper
layout 'search' layout 'search'
......
...@@ -3,13 +3,6 @@ class UsersController < ApplicationController ...@@ -3,13 +3,6 @@ class UsersController < ApplicationController
before_action :set_user before_action :set_user
def show def show
@contributed_projects = contributed_projects.joined(@user).reject(&:forked?)
@projects = PersonalProjectsFinder.new(@user).execute(current_user)
@projects = @projects.page(params[:page]).per(PER_PAGE)
@groups = @user.groups.order_id_desc
respond_to do |format| respond_to do |format|
format.html format.html
...@@ -25,6 +18,45 @@ class UsersController < ApplicationController ...@@ -25,6 +18,45 @@ class UsersController < ApplicationController
end end
end end
def groups
load_groups
respond_to do |format|
format.html { render 'show' }
format.json do
render json: {
html: view_to_html_string("shared/groups/_list", groups: @groups)
}
end
end
end
def projects
load_projects
respond_to do |format|
format.html { render 'show' }
format.json do
render json: {
html: view_to_html_string("shared/projects/_list", projects: @projects, remote: true)
}
end
end
end
def contributed
load_contributed_projects
respond_to do |format|
format.html { render 'show' }
format.json do
render json: {
html: view_to_html_string("shared/projects/_list", projects: @contributed_projects)
}
end
end
end
def calendar def calendar
calendar = contributions_calendar calendar = contributions_calendar
@timestamps = calendar.timestamps @timestamps = calendar.timestamps
...@@ -69,6 +101,20 @@ class UsersController < ApplicationController ...@@ -69,6 +101,20 @@ class UsersController < ApplicationController
limit_recent(20, params[:offset]) limit_recent(20, params[:offset])
end end
def load_projects
@projects =
PersonalProjectsFinder.new(@user).execute(current_user)
.page(params[:page]).per(PER_PAGE)
end
def load_contributed_projects
@contributed_projects = contributed_projects.joined(@user)
end
def load_groups
@groups = @user.groups.order_id_desc
end
def projects_for_current_user def projects_for_current_user
ProjectsFinder.new.execute(current_user) ProjectsFinder.new.execute(current_user)
end end
......
...@@ -10,6 +10,15 @@ module IconsHelper ...@@ -10,6 +10,15 @@ module IconsHelper
options.include?(:base) ? fa_stacked_icon(names, options) : fa_icon(names, options) options.include?(:base) ? fa_stacked_icon(names, options) : fa_icon(names, options)
end end
def audit_icon(names, options = {})
case names
when "standard"
names = "key"
end
options.include?(:base) ? fa_stacked_icon(names, options) : fa_icon(names, options)
end
def spinner(text = nil, visible = false) def spinner(text = nil, visible = false)
css_class = 'loading' css_class = 'loading'
css_class << ' hide' unless visible css_class << ' hide' unless visible
......
...@@ -36,4 +36,14 @@ module MilestonesHelper ...@@ -36,4 +36,14 @@ module MilestonesHelper
options_from_collection_for_select(grouped_milestones, 'name', 'title', params[:milestone_title]) options_from_collection_for_select(grouped_milestones, 'name', 'title', params[:milestone_title])
end end
def milestone_remaining_days(milestone)
if milestone.expired?
content_tag(:strong, 'expired')
elsif milestone.due_date
days = milestone.remaining_days
content = content_tag(:strong, days)
content << " #{'day'.pluralize(days)} remaining"
end
end
end end
...@@ -38,6 +38,7 @@ module Issuable ...@@ -38,6 +38,7 @@ module Issuable
scope :join_project, -> { joins(:project) } scope :join_project, -> { joins(:project) }
scope :references_project, -> { references(:project) } scope :references_project, -> { references(:project) }
scope :non_archived, -> { join_project.merge(Project.non_archived) }
delegate :name, delegate :name,
:email, :email,
......
...@@ -27,6 +27,7 @@ class Label < ActiveRecord::Base ...@@ -27,6 +27,7 @@ class Label < ActiveRecord::Base
belongs_to :project belongs_to :project
has_many :label_links, dependent: :destroy has_many :label_links, dependent: :destroy
has_many :issues, through: :label_links, source: :target, source_type: 'Issue' has_many :issues, through: :label_links, source: :target, source_type: 'Issue'
has_many :merge_requests, through: :label_links, source: :target, source_type: 'MergeRequest'
validates :color, color: true, allow_blank: false validates :color, color: true, allow_blank: false
validates :project, presence: true, unless: Proc.new { |service| service.template? } validates :project, presence: true, unless: Proc.new { |service| service.template? }
...@@ -90,6 +91,10 @@ class Label < ActiveRecord::Base ...@@ -90,6 +91,10 @@ class Label < ActiveRecord::Base
issues.closed.count issues.closed.count
end end
def open_merge_requests_count
merge_requests.opened.count
end
def template? def template?
template template
end end
......
...@@ -111,17 +111,10 @@ class Milestone < ActiveRecord::Base ...@@ -111,17 +111,10 @@ class Milestone < ActiveRecord::Base
0 0
end end
# Returns the elapsed time (in percent) since the Milestone creation date until today. def remaining_days
# If the Milestone doesn't have a due_date then returns 0 since we can't calculate the elapsed time. return 0 if !due_date || expired?
# If the Milestone is overdue then it returns 100%.
def percent_time_used
return 0 unless due_date
return 100 if expired?
duration = ((created_at - due_date.to_datetime) / 1.day) (due_date - Date.today).to_i
days_elapsed = ((created_at - Time.now) / 1.day)
((days_elapsed.to_f / duration) * 100).floor
end end
def expires_at def expires_at
......
...@@ -314,7 +314,7 @@ class Project < ActiveRecord::Base ...@@ -314,7 +314,7 @@ class Project < ActiveRecord::Base
end end
def search_by_title(query) def search_by_title(query)
where('projects.archived = ?', false).where('LOWER(projects.name) LIKE :query', query: "%#{query.downcase}%") non_archived.where('LOWER(projects.name) LIKE :query', query: "%#{query.downcase}%")
end end
def find_with_namespace(id) def find_with_namespace(id)
...@@ -817,6 +817,8 @@ class Project < ActiveRecord::Base ...@@ -817,6 +817,8 @@ class Project < ActiveRecord::Base
old_path_with_namespace = File.join(namespace_dir, path_was) old_path_with_namespace = File.join(namespace_dir, path_was)
new_path_with_namespace = File.join(namespace_dir, path) new_path_with_namespace = File.join(namespace_dir, path)
expire_caches_before_rename(old_path_with_namespace)
if gitlab_shell.mv_repository(old_path_with_namespace, new_path_with_namespace) if gitlab_shell.mv_repository(old_path_with_namespace, new_path_with_namespace)
# If repository moved successfully we need to send update instructions to users. # If repository moved successfully we need to send update instructions to users.
# However we cannot allow rollback since we moved repository # However we cannot allow rollback since we moved repository
...@@ -846,6 +848,22 @@ class Project < ActiveRecord::Base ...@@ -846,6 +848,22 @@ class Project < ActiveRecord::Base
Gitlab::PagesTransfer.new.rename_project(path_was, path, namespace.path) Gitlab::PagesTransfer.new.rename_project(path_was, path, namespace.path)
end end
# Expires various caches before a project is renamed.
def expire_caches_before_rename(old_path)
repo = Repository.new(old_path, self)
wiki = Repository.new("#{old_path}.wiki", self)
if repo.exists?
repo.expire_cache
repo.expire_emptiness_caches
end
if wiki.exists?
wiki.expire_cache
wiki.expire_emptiness_caches
end
end
def hook_attrs def hook_attrs
{ {
name: name, name: name,
......
...@@ -690,30 +690,38 @@ class Repository ...@@ -690,30 +690,38 @@ class Repository
end end
end end
def revert(user, commit, base_branch, target_branch = nil) def revert(user, commit, base_branch, revert_tree_id = nil)
source_sha = find_branch(base_branch).target source_sha = find_branch(base_branch).target
target_branch ||= base_branch revert_tree_id ||= check_revert_content(commit, base_branch)
args = [commit.id, source_sha]
args << { mainline: 1 } if commit.merge_commit?
revert_index = rugged.revert_commit(*args)
return false if revert_index.conflicts?
tree_id = revert_index.write_tree(rugged) return false unless revert_tree_id
return false unless diff_exists?(source_sha, tree_id)
commit_with_hooks(user, target_branch) do |ref| commit_with_hooks(user, base_branch) do |ref|
committer = user_to_committer(user) committer = user_to_committer(user)
source_sha = Rugged::Commit.create(rugged, source_sha = Rugged::Commit.create(rugged,
message: commit.revert_message, message: commit.revert_message,
author: committer, author: committer,
committer: committer, committer: committer,
tree: tree_id, tree: revert_tree_id,
parents: [rugged.lookup(source_sha)], parents: [rugged.lookup(source_sha)],
update_ref: ref) update_ref: ref)
end end
end end
def check_revert_content(commit, base_branch)
source_sha = find_branch(base_branch).target
args = [commit.id, source_sha]
args << { mainline: 1 } if commit.merge_commit?
revert_index = rugged.revert_commit(*args)
return false if revert_index.conflicts?
tree_id = revert_index.write_tree(rugged)
return false unless diff_exists?(source_sha, tree_id)
tree_id
end
def diff_exists?(sha1, sha2) def diff_exists?(sha1, sha2)
rugged.diff(sha1, sha2).size > 0 rugged.diff(sha1, sha2).size > 0
end end
......
...@@ -171,7 +171,7 @@ class User < ActiveRecord::Base ...@@ -171,7 +171,7 @@ class User < ActiveRecord::Base
validates :avatar_crop_x, :avatar_crop_y, :avatar_crop_size, validates :avatar_crop_x, :avatar_crop_y, :avatar_crop_size,
numericality: { only_integer: true }, numericality: { only_integer: true },
presence: true, presence: true,
if: ->(user) { user.avatar? } if: ->(user) { user.avatar? && user.avatar_changed? }
before_validation :generate_password, on: :create before_validation :generate_password, on: :create
before_validation :restricted_signup_domains, on: :create before_validation :restricted_signup_domains, on: :create
...@@ -402,7 +402,8 @@ class User < ActiveRecord::Base ...@@ -402,7 +402,8 @@ class User < ActiveRecord::Base
def namespace_uniq def namespace_uniq
# Return early if username already failed the first uniqueness validation # Return early if username already failed the first uniqueness validation
return if self.errors[:username].include?('has already been taken') return if self.errors.key?(:username) &&
self.errors[:username].include?('has already been taken')
namespace_name = self.username namespace_name = self.username
existing_namespace = Namespace.by_path(namespace_name) existing_namespace = Namespace.by_path(namespace_name)
......
...@@ -17,28 +17,28 @@ module Commits ...@@ -17,28 +17,28 @@ module Commits
def commit def commit
revert_into = @create_merge_request ? @commit.revert_branch_name : @target_branch revert_into = @create_merge_request ? @commit.revert_branch_name : @target_branch
revert_tree_id = repository.check_revert_content(@commit, @target_branch)
if @create_merge_request if revert_tree_id
# Temporary branch exists and contains the revert commit create_target_branch(revert_into) if @create_merge_request
return success if repository.find_branch(revert_into)
create_target_branch
end
unless repository.revert(current_user, @commit, revert_into) repository.revert(current_user, @commit, revert_into, revert_tree_id)
success
else
error_msg = "Sorry, we cannot revert this #{params[:revert_type_title]} automatically. error_msg = "Sorry, we cannot revert this #{params[:revert_type_title]} automatically.
It may have already been reverted, or a more recent commit may have updated some of its content." It may have already been reverted, or a more recent commit may have updated some of its content."
raise ReversionError, error_msg raise ReversionError, error_msg
end end
success
end end
private private
def create_target_branch def create_target_branch(new_branch)
# Temporary branch exists and contains the revert commit
return success if repository.find_branch(new_branch)
result = CreateBranchService.new(@project, current_user) result = CreateBranchService.new(@project, current_user)
.execute(@commit.revert_branch_name, @target_branch, source_project: @source_project) .execute(new_branch, @target_branch, source_project: @source_project)
if result[:status] == :error if result[:status] == :error
raise ReversionError, "There was an error creating the source branch: #{result[:message]}" raise ReversionError, "There was an error creating the source branch: #{result[:message]}"
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
%h3.page-title Report abuse %h3.page-title Report abuse
%p Please use this form to report users who create spam issues, comments or behave inappropriately. %p Please use this form to report users who create spam issues, comments or behave inappropriately.
%hr %hr
= form_for @abuse_report, html: { class: 'form-horizontal js-requires-input'} do |f| = form_for @abuse_report, html: { class: 'form-horizontal js-quick-submit js-requires-input'} do |f|
= f.hidden_field :user_id = f.hidden_field :user_id
- if @abuse_report.errors.any? - if @abuse_report.errors.any?
.alert.alert-danger .alert.alert-danger
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
.form-group .form-group
= f.label :message, class: 'control-label' = f.label :message, class: 'control-label'
.col-sm-10 .col-sm-10
= f.text_area :message, class: "form-control js-quick-submit", rows: 2, required: true, value: sanitize(@ref_url) = f.text_area :message, class: "form-control", rows: 2, required: true, value: sanitize(@ref_url)
.help-block .help-block
Explain the problem with this user. If appropriate, provide a link to the relevant issue or comment. Explain the problem with this user. If appropriate, provide a link to the relevant issue or comment.
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
.js-broadcast-message-preview .js-broadcast-message-preview
= render_broadcast_message(@broadcast_message.message.presence || "Your message here") = render_broadcast_message(@broadcast_message.message.presence || "Your message here")
= form_for [:admin, @broadcast_message], html: { class: 'broadcast-message-form form-horizontal js-requires-input'} do |f| = form_for [:admin, @broadcast_message], html: { class: 'broadcast-message-form form-horizontal js-quick-submit js-requires-input'} do |f|
-if @broadcast_message.errors.any? -if @broadcast_message.errors.any?
.alert.alert-danger .alert.alert-danger
- @broadcast_message.errors.full_messages.each do |msg| - @broadcast_message.errors.full_messages.each do |msg|
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
.form-group .form-group
= f.label :message, class: 'control-label' = f.label :message, class: 'control-label'
.col-sm-10 .col-sm-10
= f.text_area :message, class: "form-control js-quick-submit js-autosize", = f.text_area :message, class: "form-control js-autosize",
required: true, required: true,
data: { preview_path: preview_admin_broadcast_messages_path } data: { preview_path: preview_admin_broadcast_messages_path }
.form-group.js-toggle-colors-container .form-group.js-toggle-colors-container
......
.projects-list-holder = render 'shared/projects/list', projects: @projects, ci: true
= render 'shared/projects/list', projects: @projects, ci: true
:javascript
Dashboard.init()
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
- if @last_push - if @last_push
= render "events/event_last_push", event: @last_push = render "events/event_last_push", event: @last_push
- if @projects.any? - if @projects.any? || params[:filter_projects]
= render 'projects' = render 'projects'
- else - else
= render "zero_authorized_projects" = render "zero_authorized_projects"
.pull-right.hidden-sm.hidden-xs - if current_user
- if current_user .dropdown
.dropdown.inline.append-right-10
%a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
%i.fa.fa-globe = icon('globe')
%span.light Visibility: %span.light Visibility:
- if params[:visibility_level].present? - if params[:visibility_level].present?
= visibility_level_label(params[:visibility_level].to_i) = visibility_level_label(params[:visibility_level].to_i)
...@@ -19,10 +18,10 @@ ...@@ -19,10 +18,10 @@
= visibility_level_icon(level) = visibility_level_icon(level)
= visibility_level_label(level) = visibility_level_label(level)
- if @tags.present? - if @tags.present?
.dropdown.inline.append-right-10 .dropdown
%a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
%i.fa.fa-tags = icon('tags')
%span.light Tags: %span.light Tags:
- if params[:tag].present? - if params[:tag].present?
= params[:tag] = params[:tag]
......
- if projects.any? = render 'shared/projects/list', projects: projects
.projects-list-holder
= render 'shared/projects/list', projects: projects
- else
.nothing-here-block
No such projects
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
.top-area .top-area
= render 'explore/projects/nav' = render 'explore/projects/nav'
.gray-content-block.second-block.clearfix .nav-controls
= render 'filter' = render 'filter'
= render 'projects', projects: @projects = render 'projects', projects: @projects
...@@ -8,5 +8,4 @@ ...@@ -8,5 +8,4 @@
= icon('plus') = icon('plus')
New Project New Project
.projects-list-holder = render 'shared/projects/list', projects: @projects, stars: false, skip_namespace: true
= render 'shared/projects/list', projects: @projects, projects_limit: 20, stars: false, skip_namespace: true
- user = member.user - user = member.user
- return unless user || member.invite? - return unless user || member.invite?
- show_roles = local_assigns.fetch(:show_roles, true)
%li{class: "#{dom_class(member)} js-toggle-container", id: dom_id(member)} %li{class: "#{dom_class(member)} js-toggle-container", id: dom_id(member)}
%span{class: ("list-item-name" if show_controls)} %span{class: ("list-item-name" if show_controls)}
...@@ -28,7 +29,7 @@ ...@@ -28,7 +29,7 @@
= link_to resend_invite_group_group_member_path(@group, member), method: :post, class: "btn-xs btn", title: 'Resend invite' do = link_to resend_invite_group_group_member_path(@group, member), method: :post, class: "btn-xs btn", title: 'Resend invite' do
Resend invite Resend invite
- if should_user_see_group_roles?(current_user, @group) - if show_roles && should_user_see_group_roles?(current_user, @group)
%span.pull-right %span.pull-right
%strong.member-access-level= member.human_access %strong.member-access-level= member.human_access
- if show_controls - if show_controls
......
...@@ -8,18 +8,18 @@ ...@@ -8,18 +8,18 @@
This will create milestone in every selected project This will create milestone in every selected project
%hr %hr
= form_for @milestone, url: group_milestones_path(@group), html: { class: 'form-horizontal milestone-form gfm-form js-requires-input' } do |f| = form_for @milestone, url: group_milestones_path(@group), html: { class: 'form-horizontal milestone-form gfm-form js-quick-submit js-requires-input' } do |f|
.row .row
.col-md-6 .col-md-6
.form-group .form-group
= f.label :title, "Title", class: "control-label" = f.label :title, "Title", class: "control-label"
.col-sm-10 .col-sm-10
= f.text_field :title, maxlength: 255, class: "form-control js-quick-submit", required: true, autofocus: true = f.text_field :title, maxlength: 255, class: "form-control", required: true, autofocus: true
.form-group.milestone-description .form-group.milestone-description
= f.label :description, "Description", class: "control-label" = f.label :description, "Description", class: "control-label"
.col-sm-10 .col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do = render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do
= render 'projects/zen', f: f, attr: :description, classes: 'description form-control js-quick-submit' = render 'projects/zen', f: f, attr: :description, classes: 'description form-control'
.clearfix .clearfix
.error-alert .error-alert
.form-group .form-group
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
.header-logo .header-logo
%a#logo %a#logo
= brand_header_logo = brand_header_logo
= link_to root_path, class: 'home', title: 'Dashboard', id: 'js-shortcuts-home' do = link_to root_path, class: 'gitlab-text-container-link', title: 'Dashboard', id: 'js-shortcuts-home' do
.gitlab-text-container .gitlab-text-container
%h3 GitLab %h3 GitLab
......
...@@ -5,11 +5,7 @@ ...@@ -5,11 +5,7 @@
-# Ideally this would be inside the head, but turbolinks only evaluates page-specific JS in the body. -# Ideally this would be inside the head, but turbolinks only evaluates page-specific JS in the body.
= yield :scripts_body_top = yield :scripts_body_top
- if current_user
= render "layouts/header/default", title: header_title = render "layouts/header/default", title: header_title
- else
= render "layouts/header/public", title: header_title
= render 'layouts/page', sidebar: sidebar = render 'layouts/page', sidebar: sidebar
= yield :scripts_body = yield :scripts_body
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
.header-logo .header-logo
%a#logo %a#logo
= brand_header_logo = brand_header_logo
= link_to root_path, class: 'home', title: 'Dashboard', id: 'js-shortcuts-home' do = link_to root_path, class: 'gitlab-text-container-link', title: 'Dashboard', id: 'js-shortcuts-home' do
.gitlab-text-container .gitlab-text-container
%h3 GitLab %h3 GitLab
......
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
%li.visible-sm.visible-xs %li.visible-sm.visible-xs
= link_to search_path, title: 'Search', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = link_to search_path, title: 'Search', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('search') = icon('search')
- if current_user
- if session[:impersonator_id] - if session[:impersonator_id]
%li.impersonation %li.impersonation
= link_to stop_impersonation_admin_users_path, method: :delete, title: 'Stop Impersonation', data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do = link_to stop_impersonation_admin_users_path, method: :delete, title: 'Stop Impersonation', data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
...@@ -37,6 +38,10 @@ ...@@ -37,6 +38,10 @@
%li %li
= link_to destroy_user_session_path, class: 'logout', method: :delete, title: 'Sign out', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = link_to destroy_user_session_path, class: 'logout', method: :delete, title: 'Sign out', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('sign-out') = icon('sign-out')
- else
.pull-right
= link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success'
%h1.title= title %h1.title= title
......
%header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class }
%div{ class: fluid_layout ? "container-fluid" : "container-fluid" }
.header-content
- unless current_controller?('sessions')
.pull-right
= link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success'
%h1.title= title
= render 'shared/outdated_browser'
.table-holder %h5.prepend-top-0
%table.table#audits History of authentications
%thead
%tr
%th Action
%th When
%tbody %ul.well-list
- events.each do |event| - events.each do |event|
%tr %li
%td %span.description
%span = audit_icon(event.details[:with], class: "append-right-5")
Signed in with Signed in with
%b= event.details[:with] = event.details[:with]
authentication authentication
%td #{time_ago_in_words event.created_at} ago %span.pull-right
#{time_ago_in_words event.created_at} ago
= paginate events, theme: "gitlab" = paginate events, theme: "gitlab"
- page_title "Audit Log" - page_title "Audit Log"
- header_title page_title, audit_log_profile_path - header_title page_title, audit_log_profile_path
.alert.alert-help.prepend-top-default .row.prepend-top-default
History of authentications .col-lg-3.profile-settings-sidebar
%h3.prepend-top-0
.prepend-top-default = page_title
= render 'event_table', events: @events %p
This is a security log of important events involving your account.
.col-lg-9
= render 'event_table', events: @events
%li %li.notification-list-item
%span.notification.fa.fa-holder %span.notification.fa.fa-holder.append-right-5
- if notification.global? - if notification.global?
= notification_icon(@notification) = notification_icon(@notification)
- else - else
......
- page_title "Notifications" - page_title "Notifications"
- header_title page_title, profile_notifications_path - header_title page_title, profile_notifications_path
.prepend-top-default = form_for @user, url: profile_notifications_path, method: :put, html: { class: 'update-notifications prepend-top-default' } do |f|
= form_for @user, url: profile_notifications_path, method: :put, html: { class: 'update-notifications form-horizontal global-notifications-form' } do |f|
-if @user.errors.any? -if @user.errors.any?
%div.alert.alert-danger %div.alert.alert-danger
%ul %ul
...@@ -10,15 +9,22 @@ ...@@ -10,15 +9,22 @@
%li= msg %li= msg
= hidden_field_tag :notification_type, 'global' = hidden_field_tag :notification_type, 'global'
.row
.col-lg-3.profile-settings-sidebar
%h4
= page_title
%p
You can specify notification level per group or per project.
%p
By default, all projects and groups will use the global notifications setting.
.col-lg-9
%h5
Global notification settings
.form-group .form-group
= f.label :notification_email, class: "control-label" = f.label :notification_email, class: "label-light"
.col-sm-10 = f.select :notification_email, @user.all_emails, { include_blank: false }, class: "select2"
= f.select :notification_email, @user.all_emails, { include_blank: false }, class: "form-control"
.form-group .form-group
= f.label :notification_level, class: 'control-label' = f.label :notification_level, class: 'label-light'
.col-sm-10
.radio .radio
= f.label :notification_level, value: Notification::N_DISABLED do = f.label :notification_level, value: Notification::N_DISABLED do
= f.radio_button :notification_level, Notification::N_DISABLED = f.radio_button :notification_level, Notification::N_DISABLED
...@@ -47,27 +53,21 @@ ...@@ -47,27 +53,21 @@
Watch Watch
%p You will receive notifications for any activity %p You will receive notifications for any activity
.gray-content-block .prepend-top-default
= f.submit 'Save changes', class: "btn btn-create" = f.submit 'Update settings', class: "btn btn-create"
%hr
.row.all-notifications.prepend-top-default %h5
.col-md-6 Groups (#{@group_members.count})
%p %div
You can also specify notification level per group or per project.
%br
By default, all projects and groups will use the notification level set above.
%h4 Groups:
%ul.bordered-list %ul.bordered-list
- @group_members.each do |group_member| - @group_members.each do |group_member|
- notification = Notification.new(group_member) - notification = Notification.new(group_member)
= render 'settings', type: 'group', membership: group_member, notification: notification = render 'settings', type: 'group', membership: group_member, notification: notification
%h5
.col-md-6 Projects (#{@project_members.count})
%p %p.account-well
To specify the notification level per project of a group you belong to, To specify the notification level per project of a group you belong to, you need to be a member of the project itself, not only its group.
%br .append-bottom-default
you need to be a member of the project itself, not only its group.
%h4 Projects:
%ul.bordered-list %ul.bordered-list
- @project_members.each do |project_member| - @project_members.each do |project_member|
- notification = Notification.new(project_member) - notification = Notification.new(project_member)
......
- page_title 'Preferences' - page_title 'Preferences'
- header_title page_title, profile_preferences_path - header_title page_title, profile_preferences_path
.alert.alert-help.prepend-top-default = form_for @user, url: profile_preferences_path, remote: true, method: :put, html: {class: 'row prepend-top-default js-preferences-form'} do |f|
These settings allow you to customize the appearance and behavior of the site. .col-lg-3.profile-settings-sidebar
They are saved with your account and will persist to any device you use to %h4.prepend-top-0
access the site.
= form_for @user, url: profile_preferences_path, remote: true, method: :put, html: {class: 'js-preferences-form form-horizontal'} do |f|
.panel.panel-default.application-theme
.panel-heading
Application theme Application theme
.panel-body %p
This setting allows you to customize the appearance of the site, ex. sidebar.
.col-lg-9.application-theme
- Gitlab::Themes.each do |theme| - Gitlab::Themes.each do |theme|
= label_tag do = label_tag do
.preview{class: theme.css_class} .preview{class: theme.css_class}
= f.radio_button :theme_id, theme.id = f.radio_button :theme_id, theme.id
= theme.name = theme.name
.col-sm-12
.panel.panel-default.syntax-theme %hr
.panel-heading .col-lg-3.profile-settings-sidebar
%h4.prepend-top-0
Syntax highlighting theme Syntax highlighting theme
.panel-body %p
This setting allow you to customize the appearance of the syntax.
.col-lg-9.syntax-theme
- Gitlab::ColorSchemes.each do |scheme| - Gitlab::ColorSchemes.each do |scheme|
= label_tag do = label_tag do
.preview= image_tag "#{scheme.css_class}-scheme-preview.png" .preview= image_tag "#{scheme.css_class}-scheme-preview.png"
= f.radio_button :color_scheme_id, scheme.id = f.radio_button :color_scheme_id, scheme.id
= scheme.name = scheme.name
.col-sm-12
.panel.panel-default %hr
.panel-heading .col-lg-3.profile-settings-sidebar
%h4.prepend-top-0
Behavior Behavior
.panel-body %p
This setting allows you to customize the behavior of the system layout and default views.
.col-lg-9
.form-group .form-group
= f.label :layout, class: 'control-label' do = f.label :layout, class: 'label-light' do
Layout width Layout width
.col-sm-10
= f.select :layout, layout_choices, {}, class: 'form-control' = f.select :layout, layout_choices, {}, class: 'form-control'
.help-block .help-block
Choose between fixed (max. 1200px) and fluid (100%) application layout. Choose between fixed (max. 1200px) and fluid (100%) application layout.
.form-group .form-group
= f.label :dashboard, class: 'control-label' do = f.label :dashboard, class: 'label-light' do
Default Dashboard Default Dashboard
= link_to('(?)', help_page_path('profile', 'preferences') + '#default-dashboard', target: '_blank') = link_to('(?)', help_page_path('profile', 'preferences') + '#default-dashboard', target: '_blank')
.col-sm-10
= f.select :dashboard, dashboard_choices, {}, class: 'form-control' = f.select :dashboard, dashboard_choices, {}, class: 'form-control'
.form-group .form-group
= f.label :project_view, class: 'control-label' do = f.label :project_view, class: 'label-light' do
Project view Project view
= link_to('(?)', help_page_path('profile', 'preferences') + '#default-project-view', target: '_blank') = link_to('(?)', help_page_path('profile', 'preferences') + '#default-project-view', target: '_blank')
.col-sm-10
= f.select :project_view, project_view_choices, {}, class: 'form-control' = f.select :project_view, project_view_choices, {}, class: 'form-control'
.help-block .help-block
Choose what content you want to see on a project's home page. Choose what content you want to see on a project's home page.
.panel-footer .form-group
= f.submit 'Save changes', class: 'btn btn-save' = f.submit 'Save changes', class: 'btn btn-save'
.alert.alert-help.prepend-top-default = form_for @user, url: profile_path, method: :put, html: { multipart: true, class: "edit-user prepend-top-default" }, authenticity_token: true do |f|
This information will appear on your profile. = f.hidden_field :avatar_crop_x
- if current_user.ldap_user? = f.hidden_field :avatar_crop_y
Some options are unavailable for LDAP accounts = f.hidden_field :avatar_crop_size
.prepend-top-default
= form_for @user, url: profile_path, method: :put, html: { multipart: true, class: "edit_user form-horizontal" }, authenticity_token: true do |f|
-if @user.errors.any? -if @user.errors.any?
%div.alert.alert-danger %div.alert.alert-danger
%ul %ul
- @user.errors.full_messages.each do |msg| - @user.errors.full_messages.each do |msg|
%li= msg %li= msg
.row .row
.col-md-7 .col-lg-3.profile-settings-sidebar
%h4.prepend-top-0
Public Avatar
%p
- if @user.avatar?
You can change your avatar here
- if Gitlab.config.gravatar.enabled
or remove the current avatar to revert to #{link_to Gitlab.config.gravatar.host, "http://" + Gitlab.config.gravatar.host}
- else
You can upload an avatar here
- if Gitlab.config.gravatar.enabled
or change it at #{link_to Gitlab.config.gravatar.host, "http://" + Gitlab.config.gravatar.host}
.col-lg-9
.clearfix.avatar-image.append-bottom-default
= image_tag avatar_icon(@user, 160), alt: '', class: 'avatar s160'
%h5.prepend-top-0
Upload new avatar
.prepend-top-5.append-bottom-10
%a.btn.js-choose-user-avatar-button
Browse file...
%span.avatar-file-name.prepend-left-default.js-avatar-filename No file chosen
= f.file_field :avatar, class: "js-user-avatar-input hidden"
.help-block
The maximum file size allowed is 200KB.
- if @user.avatar?
%hr
= link_to 'Remove avatar', profile_avatar_path, data: { confirm: "Avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-gray"
%hr
.row
.col-lg-3.profile-settings-sidebar
%h4.prepend-top-0
Main settings
%p
This information will appear on your profile.
- if current_user.ldap_user?
Some options are unavailable for LDAP accounts
.col-lg-9
.form-group .form-group
= f.label :name, class: "control-label" = f.label :name, class: "label-light"
.col-sm-10
= f.text_field :name, class: "form-control", required: true = f.text_field :name, class: "form-control", required: true
%span.help-block Enter your name, so people you know can recognize you. %span.help-block Enter your name, so people you know can recognize you.
.form-group .form-group
= f.label :email, class: "control-label" = f.label :email, class: "label-light"
.col-sm-10
- if @user.ldap_user? && @user.ldap_email? - if @user.ldap_user? && @user.ldap_email?
= f.text_field :email, class: "form-control", required: true, readonly: true = f.text_field :email, class: "form-control", required: true, readonly: true
%span.help-block.light %span.help-block.light
...@@ -41,66 +72,30 @@ ...@@ -41,66 +72,30 @@
- else - else
%span.help-block We also use email for avatar detection if no avatar is uploaded. %span.help-block We also use email for avatar detection if no avatar is uploaded.
.form-group .form-group
= f.label :public_email, class: "control-label" = f.label :public_email, class: "label-light"
.col-sm-10
= f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email), {include_blank: 'Do not show on profile'}, class: "select2" = f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email), {include_blank: 'Do not show on profile'}, class: "select2"
%span.help-block This email will be displayed on your public profile. %span.help-block This email will be displayed on your public profile.
.form-group .form-group
= f.label :skype, class: "control-label" = f.label :skype, class: "label-light"
.col-sm-10= f.text_field :skype, class: "form-control" = f.text_field :skype, class: "form-control"
.form-group .form-group
= f.label :linkedin, class: "control-label" = f.label :linkedin, class: "label-light"
.col-sm-10= f.text_field :linkedin, class: "form-control" = f.text_field :linkedin, class: "form-control"
.form-group .form-group
= f.label :twitter, class: "control-label" = f.label :twitter, class: "label-light"
.col-sm-10= f.text_field :twitter, class: "form-control" = f.text_field :twitter, class: "form-control"
.form-group .form-group
= f.label :website_url, 'Website', class: "control-label" = f.label :website_url, 'Website', class: "label-light"
.col-sm-10= f.text_field :website_url, class: "form-control" = f.text_field :website_url, class: "form-control"
.form-group .form-group
= f.label :location, 'Location', class: "control-label" = f.label :location, 'Location', class: "label-light"
.col-sm-10= f.text_field :location, class: "form-control" = f.text_field :location, class: "form-control"
.form-group .form-group
= f.label :bio, class: "control-label" = f.label :bio, class: "label-light"
.col-sm-10
= f.text_area :bio, rows: 4, class: "form-control", maxlength: 250 = f.text_area :bio, rows: 4, class: "form-control", maxlength: 250
%span.help-block Tell us about yourself in fewer than 250 characters. %span.help-block Tell us about yourself in fewer than 250 characters.
.prepend-top-default.append-bottom-default
.col-md-5 = f.submit 'Update profile settings', class: "btn btn-success"
.light-well
= image_tag avatar_icon(@user, 160), alt: '', class: 'avatar s160'
.clearfix
.profile-avatar-form-option
%p.light
- if @user.avatar?
You can change your avatar here
- if Gitlab.config.gravatar.enabled
%br
or remove the current avatar to revert to #{link_to Gitlab.config.gravatar.host, "http://" + Gitlab.config.gravatar.host}
- else
You can upload an avatar here
- if Gitlab.config.gravatar.enabled
%br
or change it at #{link_to Gitlab.config.gravatar.host, "http://" + Gitlab.config.gravatar.host}
%hr
%a.choose-btn.btn.btn-sm.js-choose-user-avatar-button
%i.fa.fa-paperclip
%span Choose File ...
&nbsp;
%span.file_name.js-avatar-filename File name...
= f.file_field :avatar, class: "js-user-avatar-input hidden"
= f.hidden_field :avatar_crop_x
= f.hidden_field :avatar_crop_y
= f.hidden_field :avatar_crop_size
.light The maximum file size allowed is 200KB.
- if @user.avatar?
%hr
= link_to 'Remove avatar', profile_avatar_path, data: { confirm: "Avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar"
.form-actions
= f.submit 'Save changes', class: "btn btn-success"
= link_to "Cancel", user_path(current_user), class: "btn btn-cancel" = link_to "Cancel", user_path(current_user), class: "btn btn-cancel"
.modal.modal-profile-crop .modal.modal-profile-crop
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
%span.editor-file-name %span.editor-file-name
\/ \/
= text_field_tag 'file_name', params[:file_name], placeholder: "File name", = text_field_tag 'file_name', params[:file_name], placeholder: "File name",
required: true, class: 'form-control new-file-name js-quick-submit' required: true, class: 'form-control new-file-name'
.pull-right .pull-right
= select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2' = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2'
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
%a.close{href: "#", "data-dismiss" => "modal"} × %a.close{href: "#", "data-dismiss" => "modal"} ×
%h3.page-title Create New Directory %h3.page-title Create New Directory
.modal-body .modal-body
= form_tag namespace_project_create_dir_path(@project.namespace, @project, @id), method: :post, remote: false, class: 'form-horizontal js-create-dir-form js-requires-input' do = form_tag namespace_project_create_dir_path(@project.namespace, @project, @id), method: :post, remote: false, class: 'form-horizontal js-create-dir-form js-quick-submit js-requires-input' do
.form-group .form-group
= label_tag :dir_name, 'Directory name', class: 'control-label' = label_tag :dir_name, 'Directory name', class: 'control-label'
.col-sm-10 .col-sm-10
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
%h3.page-title Delete #{@blob.name} %h3.page-title Delete #{@blob.name}
.modal-body .modal-body
= form_tag namespace_project_blob_path(@project.namespace, @project, @id), method: :delete, class: 'form-horizontal js-replace-blob-form js-requires-input' do = form_tag namespace_project_blob_path(@project.namespace, @project, @id), method: :delete, class: 'form-horizontal js-replace-blob-form js-quick-submit js-requires-input' do
= render 'shared/new_commit_form', placeholder: "Delete #{@blob.name}" = render 'shared/new_commit_form', placeholder: "Delete #{@blob.name}"
.form-group .form-group
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
%a.close{href: "#", "data-dismiss" => "modal"} × %a.close{href: "#", "data-dismiss" => "modal"} ×
%h3.page-title #{title} %h3.page-title #{title}
.modal-body .modal-body
= form_tag form_path, method: method, class: 'js-upload-blob-form form-horizontal' do = form_tag form_path, method: method, class: 'js-quick-submit js-upload-blob-form form-horizontal' do
.dropzone .dropzone
.dropzone-previews.blob-upload-dropzone-previews .dropzone-previews.blob-upload-dropzone-previews
%p.dz-message.light %p.dz-message.light
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
= icon('eye') = icon('eye')
= editing_preview_title(@blob.name) = editing_preview_title(@blob.name)
= form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-requires-input js-edit-blob-form') do = form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-quick-submit js-requires-input js-edit-blob-form') do
= render 'projects/blob/editor', ref: @ref, path: @path, blob_data: @blob.data = render 'projects/blob/editor', ref: @ref, path: @path, blob_data: @blob.data
= render 'shared/new_commit_form', placeholder: "Update #{@blob.name}" = render 'shared/new_commit_form', placeholder: "Update #{@blob.name}"
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
New File New File
.file-editor .file-editor
= form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal js-new-blob-form js-requires-input') do = form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal js-new-blob-form js-quick-submit js-requires-input') do
= render 'projects/blob/editor', ref: @ref = render 'projects/blob/editor', ref: @ref
= render 'shared/new_commit_form', placeholder: "Add new file" = render 'shared/new_commit_form', placeholder: "Add new file"
......
= render 'shared/projects/list', projects: projects, use_creator_avatar: true,
forks: true, show_last_commit_as_description: true
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
== #{pluralize(@total_forks_count, 'fork')}: #{full_count_title} == #{pluralize(@total_forks_count, 'fork')}: #{full_count_title}
.nav-controls .nav-controls
= form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
= search_field_tag :filter_projects, nil, placeholder: 'Search forks', class: 'projects-list-filter project-filter-form-field form-control input-short', = search_field_tag :filter_projects, nil, placeholder: 'Search forks', class: 'projects-list-filter project-filter-form-field form-control input-short',
spellcheck: false, data: { 'filter-selector' => 'span.namespace-name' } spellcheck: false, data: { 'filter-selector' => 'span.namespace-name' }
...@@ -38,18 +39,10 @@ ...@@ -38,18 +39,10 @@
Fork Fork
.projects-list-holder = render 'projects', projects: @forks
- if @forks.blank?
%ul.content-list
%li
.nothing-here-block No forks to show
- else
= render 'shared/projects/list', projects: @forks, use_creator_avatar: true,
forks: true, show_last_commit_as_description: true
- if @private_forks_count > 0 - if @private_forks_count > 0
%ul.projects-list.private-forks-notice .private-forks-notice
%li.project-row
= icon('lock fw', base: 'circle', class: 'fa-lg private-fork-icon') = icon('lock fw', base: 'circle', class: 'fa-lg private-fork-icon')
%strong= pluralize(@private_forks_count, 'private fork') %strong= pluralize(@private_forks_count, 'private fork')
%span you have no access to. %span you have no access to.
= form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'form-horizontal issue-form gfm-form js-requires-input' } do |f| = form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'form-horizontal issue-form gfm-form js-quick-submit js-requires-input' } do |f|
= render 'shared/issuable/form', f: f, issuable: @issue = render 'shared/issuable/form', f: f, issuable: @issue
:javascript :javascript
......
...@@ -29,6 +29,7 @@ ...@@ -29,6 +29,7 @@
%span.creator %span.creator
&middot; &middot;
by #{link_to_member(@project, @issue.author, size: 24)} by #{link_to_member(@project, @issue.author, size: 24)}
= '@' + @issue.author.username
&middot; &middot;
= time_ago_with_tooltip(@issue.created_at, placement: 'bottom', html_class: 'issue_created_ago') = time_ago_with_tooltip(@issue.created_at, placement: 'bottom', html_class: 'issue_created_ago')
......
= form_for [@project.namespace.becomes(Namespace), @project, @label], html: { class: 'form-horizontal label-form js-requires-input' } do |f| = form_for [@project.namespace.becomes(Namespace), @project, @label], html: { class: 'form-horizontal label-form js-quick-submit js-requires-input' } do |f|
-if @label.errors.any? -if @label.errors.any?
.row .row
.col-sm-offset-2.col-sm-10 .col-sm-offset-2.col-sm-10
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
.form-group .form-group
= f.label :title, class: 'control-label' = f.label :title, class: 'control-label'
.col-sm-10 .col-sm-10
= f.text_field :title, class: "form-control js-quick-submit", required: true, autofocus: true = f.text_field :title, class: "form-control", required: true, autofocus: true
.form-group .form-group
= f.label :description, class: 'control-label' = f.label :description, class: 'control-label'
.col-sm-10 .col-sm-10
......
...@@ -2,6 +2,10 @@ ...@@ -2,6 +2,10 @@
= render "shared/label_row", label: label = render "shared/label_row", label: label
.pull-right .pull-right
%strong.append-right-20
= link_to_label(label, type: :merge_request) do
= pluralize label.open_merge_requests_count, 'open merge request'
%strong.append-right-20 %strong.append-right-20
= link_to_label(label) do = link_to_label(label) do
= pluralize label.open_issues_count, 'open issue' = pluralize label.open_issues_count, 'open issue'
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
%span.creator %span.creator
&middot; &middot;
by #{link_to_member(@project, @merge_request.author, size: 24)} by #{link_to_member(@project, @merge_request.author, size: 24)}
= '@' + @merge_request.author.username
&middot; &middot;
= time_ago_with_tooltip(@merge_request.created_at) = time_ago_with_tooltip(@merge_request.created_at)
......
- status_class = @ci_commit ? " ci-#{@ci_commit.status}" : nil - status_class = @ci_commit ? " ci-#{@ci_commit.status}" : nil
= form_for [:merge, @project.namespace.becomes(Namespace), @project, @merge_request], remote: true, method: :post, html: { class: 'accept-mr-form js-requires-input' } do |f| = form_for [:merge, @project.namespace.becomes(Namespace), @project, @merge_request], remote: true, method: :post, html: { class: 'accept-mr-form js-quick-submit js-requires-input' } do |f|
= hidden_field_tag :authenticity_token, form_authenticity_token = hidden_field_tag :authenticity_token, form_authenticity_token
.accept-merge-holder.clearfix.js-toggle-container .accept-merge-holder.clearfix.js-toggle-container
.clearfix .clearfix
......
= form_for [@project.namespace.becomes(Namespace), @project, @milestone], html: {class: 'form-horizontal milestone-form gfm-form js-requires-input'} do |f| = form_for [@project.namespace.becomes(Namespace), @project, @milestone], html: {class: 'form-horizontal milestone-form gfm-form js-quick-submit js-requires-input'} do |f|
-if @milestone.errors.any? -if @milestone.errors.any?
.alert.alert-danger .alert.alert-danger
%ul %ul
...@@ -9,12 +9,12 @@ ...@@ -9,12 +9,12 @@
.form-group .form-group
= f.label :title, "Title", class: "control-label" = f.label :title, "Title", class: "control-label"
.col-sm-10 .col-sm-10
= f.text_field :title, maxlength: 255, class: "form-control js-quick-submit", required: true, autofocus: true = f.text_field :title, maxlength: 255, class: "form-control", required: true, autofocus: true
.form-group.milestone-description .form-group.milestone-description
= f.label :description, "Description", class: "control-label" = f.label :description, "Description", class: "control-label"
.col-sm-10 .col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do = render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do
= render 'projects/zen', f: f, attr: :description, classes: 'description form-control js-quick-submit' = render 'projects/zen', f: f, attr: :description, classes: 'description form-control'
= render 'projects/notes/hints' = render 'projects/notes/hints'
.clearfix .clearfix
.error-alert .error-alert
......
...@@ -60,9 +60,7 @@ ...@@ -60,9 +60,7 @@
%strong== #{@milestone.percent_complete}% %strong== #{@milestone.percent_complete}%
complete complete
%span.milestone-stat %span.milestone-stat
%span.time-elapsed %span.remaining-days= milestone_remaining_days(@milestone)
%strong== #{@milestone.percent_time_used}%
time elapsed
%span.pull-right.tab-issues-buttons %span.pull-right.tab-issues-buttons
- 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: { milestone_id: @milestone.id }), class: "btn btn-grouped", title: "New Issue" do = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { milestone_id: @milestone.id }), class: "btn btn-grouped", title: "New Issue" do
......
.note-edit-form .note-edit-form
= form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true do |f| = form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true, class: 'js-quick-submit' do |f|
= note_target_fields(note) = note_target_fields(note)
= render layout: 'projects/md_preview', locals: { preview_class: 'md-preview' } do = render layout: 'projects/md_preview', locals: { preview_class: 'md-preview' } do
= render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text js-task-list-field js-quick-submit' = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text js-task-list-field'
= render 'projects/notes/hints' = render 'projects/notes/hints'
.note-form-actions .note-form-actions
......
= form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new_note js-new-note-form common-note-form gfm-form" }, authenticity_token: true do |f| = form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new_note js-new-note-form js-quick-submit common-note-form gfm-form" }, authenticity_token: true do |f|
= hidden_field_tag :view, diff_view = hidden_field_tag :view, diff_view
= hidden_field_tag :line_type = hidden_field_tag :line_type
= note_target_fields(@note) = note_target_fields(@note)
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
= f.hidden_field :noteable_type = f.hidden_field :noteable_type
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
= render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text js-quick-submit' = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text'
= render 'projects/notes/hints' = render 'projects/notes/hints'
.error-alert .error-alert
......
...@@ -9,9 +9,9 @@ ...@@ -9,9 +9,9 @@
%strong #{@tag.name} %strong #{@tag.name}
.prepend-top-default .prepend-top-default
= form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal gfm-form release-form' }) do |f| = form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal gfm-form release-form js-quick-submit' }) do |f|
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
= render 'projects/zen', f: f, attr: :description, classes: 'description js-quick-submit form-control' = render 'projects/zen', f: f, attr: :description, classes: 'description form-control'
= render 'projects/notes/hints' = render 'projects/notes/hints'
.error-alert .error-alert
.form-actions.prepend-top-default .form-actions.prepend-top-default
......
$('.js-totaltags-count').html("#{@repository.tags.size}");
- if @repository.tags.empty?
$('.tags').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000)
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
New Tag New Tag
%hr %hr
= form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal gfm-form tag-form js-requires-input" do = form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal gfm-form tag-form js-quick-submit js-requires-input" do
.form-group .form-group
= label_tag :tag_name, nil, class: 'control-label' = label_tag :tag_name, nil, class: 'control-label'
.col-sm-10 .col-sm-10
...@@ -30,7 +30,7 @@ ...@@ -30,7 +30,7 @@
= label_tag :release_description, 'Release notes', class: 'control-label' = label_tag :release_description, 'Release notes', class: 'control-label'
.col-sm-10 .col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
= render 'projects/zen', attr: :release_description, classes: 'description js-quick-submit form-control' = render 'projects/zen', attr: :release_description, classes: 'description form-control'
= render 'projects/notes/hints' = render 'projects/notes/hints'
.help-block Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page. .help-block Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page.
.form-actions .form-actions
......
= form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form gfm-form prepend-top-default' } do |f| = form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form gfm-form prepend-top-default js-quick-submit' } do |f|
-if @page.errors.any? -if @page.errors.any?
#error_explanation #error_explanation
.alert.alert-danger .alert.alert-danger
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
= f.label :content, class: 'control-label' = f.label :content, class: 'control-label'
.col-sm-10 .col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do = render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do
= render 'projects/zen', f: f, attr: :content, classes: 'description form-control js-quick-submit' = render 'projects/zen', f: f, attr: :content, classes: 'description form-control'
= render 'projects/notes/hints' = render 'projects/notes/hints'
.clearfix .clearfix
......
...@@ -11,4 +11,4 @@ ...@@ -11,4 +11,4 @@
= button_tag 'Search', class: "btn btn-primary" = button_tag 'Search', class: "btn btn-primary"
- unless params[:snippets].eql? 'true' - unless params[:snippets].eql? 'true'
%br %br
= render 'filter' = render 'filter' if current_user
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
.max-width-marker .max-width-marker
= text_area_tag 'commit_message', = text_area_tag 'commit_message',
(params[:commit_message] || local_assigns[:text]), (params[:commit_message] || local_assigns[:text]),
class: 'form-control js-commit-message js-quick-submit', placeholder: local_assigns[:placeholder], class: 'form-control js-commit-message', placeholder: local_assigns[:placeholder],
required: true, rows: (local_assigns[:rows] || 3), required: true, rows: (local_assigns[:rows] || 3),
id: "commit_message-#{nonce}" id: "commit_message-#{nonce}"
- if local_assigns[:hint] - if local_assigns[:hint]
......
- if groups.any?
%ul.content-list
- groups.each_with_index do |group, i|
= render "shared/groups/group", group: group
- else
%h3 No groups found
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
= f.label :title, class: 'control-label' = f.label :title, class: 'control-label'
.col-sm-10 .col-sm-10
= f.text_field :title, maxlength: 255, autofocus: true, autocomplete: 'off', = f.text_field :title, maxlength: 255, autofocus: true, autocomplete: 'off',
class: 'form-control pad js-gfm-input js-quick-submit', required: true class: 'form-control pad js-gfm-input', required: true
- if issuable.is_a?(MergeRequest) - if issuable.is_a?(MergeRequest)
%p.help-block %p.help-block
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
= render 'projects/zen', f: f, attr: :description, = render 'projects/zen', f: f, attr: :description,
classes: 'description form-control js-quick-submit' classes: 'description form-control'
= render 'projects/notes/hints' = render 'projects/notes/hints'
.clearfix .clearfix
.error-alert .error-alert
......
...@@ -6,25 +6,19 @@ ...@@ -6,25 +6,19 @@
- ci = false unless local_assigns[:ci] == true - ci = false unless local_assigns[:ci] == true
- skip_namespace = false unless local_assigns[:skip_namespace] == true - skip_namespace = false unless local_assigns[:skip_namespace] == true
- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true - show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true
- remote = false unless local_assigns[:remote] == true
%ul.projects-list.content-list .projects-list-holder
- if projects.any? - if projects.any?
%ul.projects-list.content-list
- projects.each_with_index do |project, i| - projects.each_with_index do |project, i|
- css_class = (i >= projects_limit) ? 'hide' : nil - css_class = (i >= projects_limit) ? 'hide' : nil
= render "shared/projects/project", project: project, skip_namespace: skip_namespace, = render "shared/projects/project", project: project, skip_namespace: skip_namespace,
avatar: avatar, stars: stars, css_class: css_class, ci: ci, use_creator_avatar: use_creator_avatar, avatar: avatar, stars: stars, css_class: css_class, ci: ci, use_creator_avatar: use_creator_avatar,
forks: forks, show_last_commit_as_description: show_last_commit_as_description forks: forks, show_last_commit_as_description: show_last_commit_as_description
= paginate(projects, remote: remote, theme: "gitlab") if projects.respond_to? :total_pages
- if projects.size > projects_limit && projects.kind_of?(Array)
%li.bottom.center
.light
#{projects_limit} of #{pluralize(projects.count, 'project')} displayed.
= link_to '#', class: 'js-expand' do
Show all
= paginate projects, theme: "gitlab" if projects.respond_to? :total_pages
- else - else
%h3 No projects found .nothing-here-block No projects found
:javascript :javascript
new ProjectsList(); ProjectsList.init();
Dashboard.init();
...@@ -8,7 +8,8 @@ ...@@ -8,7 +8,8 @@
= render 'shared/show_aside' = render 'shared/show_aside'
.cover-block .user-profile
.cover-block
.cover-controls .cover-controls
- if @user == current_user - if @user == current_user
= link_to profile_path, class: 'btn btn-gray' do = link_to profile_path, class: 'btn btn-gray' do
...@@ -69,56 +70,48 @@ ...@@ -69,56 +70,48 @@
= icon('map-marker') = icon('map-marker')
= @user.location = @user.location
%ul.nav-links.center %ul.nav-links.center.user-profile-nav
%li.active %li.activity-tab
= link_to "#activity", 'data-toggle' => 'tab' do = link_to user_calendar_activities_path, data: {target: 'div#activity', action: 'activity', toggle: 'tab'} do
Activity Activity
- if @groups.any? %li.groups-tab
%li = link_to user_groups_path, data: {target: 'div#groups', action: 'groups', toggle: 'tab'} do
= link_to "#groups", 'data-toggle' => 'tab' do
Groups Groups
- if @contributed_projects.present? %li.contributed-tab
%li = link_to user_contributed_projects_path, data: {target: 'div#contributed', action: 'contributed', toggle: 'tab'} do
= link_to "#contributed", 'data-toggle' => 'tab' do
Contributed projects Contributed projects
- if @projects.present? %li.projects-tab
%li = link_to user_projects_path, data: {target: 'div#projects', action: 'projects', toggle: 'tab'} do
= link_to "#personal", 'data-toggle' => 'tab' do
Personal projects Personal projects
%div{ class: container_class } %div{ class: container_class }
.tab-content .tab-content
.tab-pane.active#activity #activity.tab-pane
.gray-content-block.white.second-block .gray-content-block.white.second-block
%div{ class: container_class } %div{ class: container_class }
.user-calendar .user-calendar{data: {href: user_calendar_path}}
%h4.center.light %h4.center.light
%i.fa.fa-spinner.fa-spin %i.fa.fa-spinner.fa-spin
.user-calendar-activities .user-calendar-activities
.content_list{ data: {href: user_path} }
.content_list
= spinner = spinner
- if @groups.any? #groups.tab-pane
.tab-pane#groups - # This tab is always loaded via AJAX
%ul.content-list
- @groups.each do |group| #contributed.contributed-projects.tab-pane
= render 'shared/groups/group', group: group - # This tab is always loaded via AJAX
- if @contributed_projects.present? #projects.tab-pane
.tab-pane#contributed - # This tab is always loaded via AJAX
.contributed-projects
= render 'shared/projects/list', .loading-status
projects: @contributed_projects.sort_by(&:star_count).reverse, = spinner
projects_limit: 10, stars: true, avatar: true
- if @projects.present?
.tab-pane#personal
.personal-projects
= render 'shared/projects/list',
projects: @projects.sort_by(&:star_count).reverse,
projects_limit: 10, stars: true, avatar: true
:javascript :javascript
$(".user-calendar").load("#{user_calendar_path}"); var userProfile;
userProfile = new User({
action: "#{controller.action_name}"
});
...@@ -54,14 +54,6 @@ module Gitlab ...@@ -54,14 +54,6 @@ module Gitlab
config.action_view.sanitized_allowed_protocols = %w(smb) config.action_view.sanitized_allowed_protocols = %w(smb)
# Relative URL support
# WARNING: We recommend using an FQDN to host GitLab in a root path instead
# of using a relative URL.
# Documentation: http://doc.gitlab.com/ce/install/relative_url.html
# Uncomment and customize the following line to run in a non-root path
#
# config.relative_url_root = "/gitlab"
config.middleware.use Rack::Attack config.middleware.use Rack::Attack
# Allow access to GitLab API from other domains # Allow access to GitLab API from other domains
......
...@@ -276,10 +276,6 @@ Settings.gitlab_ci['builds_path'] = File.expand_path(Settings.gitlab_c ...@@ -276,10 +276,6 @@ Settings.gitlab_ci['builds_path'] = File.expand_path(Settings.gitlab_c
# #
Settings['incoming_email'] ||= Settingslogic.new({}) Settings['incoming_email'] ||= Settingslogic.new({})
Settings.incoming_email['enabled'] = false if Settings.incoming_email['enabled'].nil? Settings.incoming_email['enabled'] = false if Settings.incoming_email['enabled'].nil?
Settings.incoming_email['port'] = 143 if Settings.incoming_email['port'].nil?
Settings.incoming_email['ssl'] = false if Settings.incoming_email['ssl'].nil?
Settings.incoming_email['start_tls'] = false if Settings.incoming_email['start_tls'].nil?
Settings.incoming_email['mailbox'] = "inbox" if Settings.incoming_email['mailbox'].nil?
# #
# Build Artifacts # Build Artifacts
......
# Relative URL support
# WARNING: We recommend using an FQDN to host GitLab in a root path instead
# of using a relative URL.
# Documentation: http://doc.gitlab.com/ce/install/relative_url.html
# Copy this file to relative_url.rb and customize it to run in a non-root path
#
Rails.application.configure do
config.relative_url_root = "/gitlab"
end
:mailboxes: :mailboxes:
<% <%
require_relative 'config/environment.rb' require "yaml"
require "json"
if Gitlab::IncomingEmail.enabled? rails_env = ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
config = Gitlab::IncomingEmail.config
config_file = ENV["MAIL_ROOM_GITLAB_CONFIG_FILE"] || "config/gitlab.yml"
if File.exists?(config_file)
all_config = YAML.load_file(config_file)[rails_env]
config = all_config["incoming_email"] || {}
config['enabled'] = false if config['enabled'].nil?
config['port'] = 143 if config['port'].nil?
config['ssl'] = false if config['ssl'].nil?
config['start_tls'] = false if config['start_tls'].nil?
config['mailbox'] = "inbox" if config['mailbox'].nil?
if config['enabled'] && config['address'] && config['address'].include?('%{key}')
redis_config_file = "config/resque.yml" redis_config_file = "config/resque.yml"
redis_url = redis_url =
if File.exists?(redis_config_file) if File.exists?(redis_config_file)
YAML.load_file(redis_config_file)[Rails.env] YAML.load_file(redis_config_file)[rails_env]
else else
"redis://localhost:6379" "redis://localhost:6379"
end end
%> %>
- -
:host: <%= config.host.to_json %> :host: <%= config['host'].to_json %>
:port: <%= config.port.to_json %> :port: <%= config['port'].to_json %>
:ssl: <%= config.ssl.to_json %> :ssl: <%= config['ssl'].to_json %>
:start_tls: <%= config.start_tls.to_json %> :start_tls: <%= config['start_tls'].to_json %>
:email: <%= config.user.to_json %> :email: <%= config['user'].to_json %>
:password: <%= config.password.to_json %> :password: <%= config['password'].to_json %>
:name: <%= config.mailbox.to_json %> :name: <%= config['mailbox'].to_json %>
:delete_after_delivery: true :delete_after_delivery: true
...@@ -36,4 +48,5 @@ if Gitlab::IncomingEmail.enabled? ...@@ -36,4 +48,5 @@ if Gitlab::IncomingEmail.enabled?
:arbitration_options: :arbitration_options:
:redis_url: <%= redis_url.to_json %> :redis_url: <%= redis_url.to_json %>
:namespace: mail_room:gitlab :namespace: mail_room:gitlab
<% end %>
<% end %> <% end %>
...@@ -338,6 +338,15 @@ Rails.application.routes.draw do ...@@ -338,6 +338,15 @@ Rails.application.routes.draw do
get 'u/:username/calendar_activities' => 'users#calendar_activities', as: :user_calendar_activities, get 'u/:username/calendar_activities' => 'users#calendar_activities', as: :user_calendar_activities,
constraints: { username: /.*/ } constraints: { username: /.*/ }
get 'u/:username/groups' => 'users#groups', as: :user_groups,
constraints: { username: /.*/ }
get 'u/:username/projects' => 'users#projects', as: :user_projects,
constraints: { username: /.*/ }
get 'u/:username/contributed' => 'users#contributed', as: :user_contributed_projects,
constraints: { username: /.*/ }
get '/u/:username' => 'users#show', as: :user, get '/u/:username' => 'users#show', as: :user,
constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ } constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }
......
...@@ -515,7 +515,7 @@ Parameters: ...@@ -515,7 +515,7 @@ Parameters:
} }
``` ```
## Comments on merge requets ## Comments on merge requests
Comments are done via the [notes](notes.md) resource. Comments are done via the [notes](notes.md) resource.
......
...@@ -85,9 +85,19 @@ Inside the document: ...@@ -85,9 +85,19 @@ Inside the document:
## Notes ## Notes
- Notes should be in italics with the word `Note:` being bold. Use this form: - Notes should be quoted with the word `Note:` being bold. Use this form:
`_**Note:** This is something to note._`. If the note spans across multiple
lines it's OK to split the line. ```
>**Note:**
This is something to note.
```
which renders to:
>**Note:**
This is something to note.
If the note spans across multiple lines it's OK to split the line.
## New features ## New features
......
...@@ -25,7 +25,7 @@ that points to your GitLab instance. ...@@ -25,7 +25,7 @@ that points to your GitLab instance.
The TL;DR list of configuration files that you need to change in order to The TL;DR list of configuration files that you need to change in order to
serve GitLab under a relative URL is: serve GitLab under a relative URL is:
- `/home/git/gitlab/config/application.rb` - `/home/git/gitlab/config/initializers/relative_url.rb`
- `/home/git/gitlab/config/gitlab.yml` - `/home/git/gitlab/config/gitlab.yml`
- `/home/git/gitlab/config/unicorn.rb` - `/home/git/gitlab/config/unicorn.rb`
- `/home/git/gitlab-shell/config.yml` - `/home/git/gitlab-shell/config.yml`
...@@ -66,8 +66,14 @@ Make sure to follow all steps below: ...@@ -66,8 +66,14 @@ Make sure to follow all steps below:
sudo service gitlab stop sudo service gitlab stop
``` ```
1. Edit `/home/git/gitlab/config/application.rb` and uncomment/change the 1. Create `/home/git/gitlab/config/initializers/relative_url.rb`
following line:
```shell
cp /home/git/gitlab/config/initializers/relative_url.rb.sample \
/home/git/gitlab/config/initializers/relative_url.rb
```
and change the following line:
```ruby ```ruby
config.relative_url_root = "/gitlab" config.relative_url_root = "/gitlab"
...@@ -119,8 +125,12 @@ Make sure to follow all steps below: ...@@ -119,8 +125,12 @@ Make sure to follow all steps below:
### Disable relative URL in GitLab ### Disable relative URL in GitLab
To disable the relative URL, follow the same steps as above and set up the To disable the relative URL:
GitLab URL to one that doesn't contain a relative path.
1. Remove `/home/git/gitlab/config/initializers/relative_url.rb`
1. Follow the same as above starting from 2. and set up the
GitLab URL to one that doesn't contain a relative path.
[omnibus-rel]: http://doc.gitlab.com/omnibus/settings/configuration.html#configuring-a-relative-url-for-gitlab "How to setup relative URL in Omnibus GitLab" [omnibus-rel]: http://doc.gitlab.com/omnibus/settings/configuration.html#configuring-a-relative-url-for-gitlab "How to setup relative URL in Omnibus GitLab"
[restart gitlab]: ../administration/restart_gitlab.md#installations-from-source "How to restart GitLab" [restart gitlab]: ../administration/restart_gitlab.md#installations-from-source "How to restart GitLab"
# OmniAuth # OmniAuth
GitLab leverages OmniAuth to allow users to sign in using Twitter, GitHub, and other popular services. GitLab leverages OmniAuth to allow users to sign in using Twitter, GitHub, and
other popular services.
Configuring OmniAuth does not prevent standard GitLab authentication or LDAP (if configured) from continuing to work. Users can choose to sign in using any of the configured mechanisms. Configuring OmniAuth does not prevent standard GitLab authentication or LDAP
(if configured) from continuing to work. Users can choose to sign in using any
of the configured mechanisms.
- [Initial OmniAuth Configuration](#initial-omniauth-configuration) - [Initial OmniAuth Configuration](#initial-omniauth-configuration)
- [Supported Providers](#supported-providers) - [Supported Providers](#supported-providers)
...@@ -28,17 +31,25 @@ contains some settings that are common for all providers. ...@@ -28,17 +31,25 @@ contains some settings that are common for all providers.
## Initial OmniAuth Configuration ## Initial OmniAuth Configuration
Before configuring individual OmniAuth providers there are a few global settings that are in common for all providers that we need to consider. Before configuring individual OmniAuth providers there are a few global settings
that are in common for all providers that we need to consider.
- Omniauth needs to be enabled, see details below for example. - Omniauth needs to be enabled, see details below for example.
- `allow_single_sign_on` defaults to `false`. If `false` users must be created manually or they will not be able to - `allow_single_sign_on` allows you to specify the providers you want to allow to
sign in via OmniAuth. automatically create an account. It defaults to `false`. If `false` users must
- `block_auto_created_users` defaults to `true`. If `true` auto created users will be blocked by default and will be created manually or they will not be able to sign in via OmniAuth.
have to be unblocked by an administrator before they are able to sign in. - `block_auto_created_users` defaults to `true`. If `true` auto created users will
- **Note:** If you set `allow_single_sign_on` to `true` and `block_auto_created_users` to `false` please be aware be blocked by default and will have to be unblocked by an administrator before
that any user on the Internet will be able to successfully sign in to your GitLab without administrative approval. they are able to sign in.
If you want to change these settings: >**Note:**
If you set `block_auto_created_users` to `false`, make sure to only
define providers under `allow_single_sign_on` that you are able to control, like
SAML, Shibboleth, Crowd or Google, or set it to `false` otherwise any user on
the Internet will be able to successfully sign in to your GitLab without
administrative approval.
To change these settings:
* **For omnibus package** * **For omnibus package**
...@@ -48,11 +59,16 @@ If you want to change these settings: ...@@ -48,11 +59,16 @@ If you want to change these settings:
sudo editor /etc/gitlab/gitlab.rb sudo editor /etc/gitlab/gitlab.rb
``` ```
and change and change:
``` ```ruby
gitlab_rails['omniauth_enabled'] = true gitlab_rails['omniauth_enabled'] = true
gitlab_rails['omniauth_allow_single_sign_on'] = false
# CAUTION!
# This allows users to login without having a user account first. Define the allowed providers
# using an array, e.g. ["saml", "twitter"], or as true/false to allow all providers or none.
# User accounts will be created automatically when authentication was successful.
gitlab_rails['omniauth_allow_single_sign_on'] = ['saml', 'twitter']
gitlab_rails['omniauth_block_auto_created_users'] = true gitlab_rails['omniauth_block_auto_created_users'] = true
``` ```
...@@ -66,43 +82,57 @@ If you want to change these settings: ...@@ -66,43 +82,57 @@ If you want to change these settings:
sudo -u git -H editor config/gitlab.yml sudo -u git -H editor config/gitlab.yml
``` ```
and change the following section and change the following section:
``` ```yaml
## OmniAuth settings ## OmniAuth settings
omniauth: omniauth:
# Allow login via Twitter, Google, etc. using OmniAuth providers # Allow login via Twitter, Google, etc. using OmniAuth providers
enabled: true enabled: true
# CAUTION! # CAUTION!
# This allows users to login without having a user account first (default: false). # This allows users to login without having a user account first. Define the allowed providers
# using an array, e.g. ["saml", "twitter"], or as true/false to allow all providers or none.
# User accounts will be created automatically when authentication was successful. # User accounts will be created automatically when authentication was successful.
allow_single_sign_on: false allow_single_sign_on: ["saml", "twitter"]
# Locks down those users until they have been cleared by the admin (default: true). # Locks down those users until they have been cleared by the admin (default: true).
block_auto_created_users: true block_auto_created_users: true
``` ```
Now we can choose one or more of the Supported Providers below to continue configuration. Now we can choose one or more of the Supported Providers listed above to continue
the configuration process.
## Enable OmniAuth for an Existing User ## Enable OmniAuth for an Existing User
Existing users can enable OmniAuth for specific providers after the account is created. For example, if the user originally signed in with LDAP an OmniAuth provider such as Twitter can be enabled. Follow the steps below to enable an OmniAuth provider for an existing user. Existing users can enable OmniAuth for specific providers after the account is
created. For example, if the user originally signed in with LDAP, an OmniAuth
provider such as Twitter can be enabled. Follow the steps below to enable an
OmniAuth provider for an existing user.
1. Sign in normally - whether standard sign in, LDAP, or another OmniAuth provider. 1. Sign in normally - whether standard sign in, LDAP, or another OmniAuth provider.
1. Go to profile settings (the silhouette icon in the top right corner). 1. Go to profile settings (the silhouette icon in the top right corner).
1. Select the "Account" tab. 1. Select the "Account" tab.
1. Under "Connected Accounts" select the desired OmniAuth provider, such as Twitter. 1. Under "Connected Accounts" select the desired OmniAuth provider, such as Twitter.
1. The user will be redirected to the provider. Once the user authorized GitLab they will be redirected back to GitLab. 1. The user will be redirected to the provider. Once the user authorized GitLab
they will be redirected back to GitLab.
The chosen OmniAuth provider is now active and can be used to sign in to GitLab from then on. The chosen OmniAuth provider is now active and can be used to sign in to GitLab from then on.
## Using Custom Omniauth Providers ## Using Custom Omniauth Providers
GitLab uses [Omniauth](http://www.omniauth.org/) for authentication and already ships with a few providers preinstalled (e.g. LDAP, GitHub, Twitter). But sometimes that is not enough and you need to integrate with other authentication solutions. For these cases you can use the Omniauth provider. >**Note:**
The following information only applies for installations from source.
GitLab uses [Omniauth](http://www.omniauth.org/) for authentication and already ships
with a few providers pre-installed (e.g. LDAP, GitHub, Twitter). But sometimes that
is not enough and you need to integrate with other authentication solutions. For
these cases you can use the Omniauth provider.
### Steps ### Steps
These steps are fairly general and you will need to figure out the exact details from the Omniauth provider's documentation. These steps are fairly general and you will need to figure out the exact details
from the Omniauth provider's documentation.
- Stop GitLab: - Stop GitLab:
...@@ -128,8 +158,12 @@ These steps are fairly general and you will need to figure out the exact details ...@@ -128,8 +158,12 @@ These steps are fairly general and you will need to figure out the exact details
### Examples ### Examples
If you have successfully set up a provider that is not shipped with GitLab itself, please let us know. If you have successfully set up a provider that is not shipped with GitLab itself,
please let us know.
You can help others by reporting successful configurations and probably share a few insights or provide warnings for common errors or pitfalls by sharing your experience [in the public Wiki](https://github.com/gitlabhq/gitlab-public-wiki/wiki/Custom-omniauth-provider-configurations). You can help others by reporting successful configurations and probably share a
few insights or provide warnings for common errors or pitfalls by sharing your
experience [in the public Wiki](https://github.com/gitlabhq/gitlab-public-wiki/wiki/Custom-omniauth-provider-configurations).
While we can't officially support every possible authentication mechanism out there, we'd like to at least help those with specific needs. While we can't officially support every possible authentication mechanism out there,
we'd like to at least help those with specific needs.
# SAML OmniAuth Provider # SAML OmniAuth Provider
GitLab can be configured to act as a SAML 2.0 Service Provider (SP). This allows GitLab to consume assertions from a SAML 2.0 Identity Provider (IdP) such as Microsoft ADFS to authenticate users. GitLab can be configured to act as a SAML 2.0 Service Provider (SP). This allows
GitLab to consume assertions from a SAML 2.0 Identity Provider (IdP) such as
Microsoft ADFS to authenticate users.
First configure SAML 2.0 support in GitLab, then register the GitLab application in your SAML IdP: First configure SAML 2.0 support in GitLab, then register the GitLab application
in your SAML IdP:
1. Make sure GitLab is configured with HTTPS. See [Using HTTPS](../install/installation.md#using-https) for instructions. 1. Make sure GitLab is configured with HTTPS.
See [Using HTTPS](../install/installation.md#using-https) for instructions.
1. On your GitLab server, open the configuration file. 1. On your GitLab server, open the configuration file.
...@@ -22,7 +26,40 @@ First configure SAML 2.0 support in GitLab, then register the GitLab application ...@@ -22,7 +26,40 @@ First configure SAML 2.0 support in GitLab, then register the GitLab application
sudo -u git -H editor config/gitlab.yml sudo -u git -H editor config/gitlab.yml
``` ```
1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings. 1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration)
for initial settings.
1. To allow your users to use SAML to sign up without having to manually create
an account first, don't forget to add the following values to your configuration:
For omnibus package:
```ruby
gitlab_rails['omniauth_allow_single_sign_on'] = ['saml']
gitlab_rails['omniauth_block_auto_created_users'] = false
```
For installations from source:
```yaml
allow_single_sign_on: ["saml"]
block_auto_created_users: false
```
1. You can also automatically link SAML users with existing GitLab users if their
email addresses match by adding the following setting:
For omnibus package:
```ruby
gitlab_rails['omniauth_auto_link_saml_user'] = true
```
For installations from source:
```yaml
auto_link_saml_user: true
```
1. Add the provider configuration: 1. Add the provider configuration:
...@@ -31,7 +68,7 @@ First configure SAML 2.0 support in GitLab, then register the GitLab application ...@@ -31,7 +68,7 @@ First configure SAML 2.0 support in GitLab, then register the GitLab application
```ruby ```ruby
gitlab_rails['omniauth_providers'] = [ gitlab_rails['omniauth_providers'] = [
{ {
"name" => "saml", name: 'saml',
args: { args: {
assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback', assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8', idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
...@@ -39,7 +76,7 @@ First configure SAML 2.0 support in GitLab, then register the GitLab application ...@@ -39,7 +76,7 @@ First configure SAML 2.0 support in GitLab, then register the GitLab application
issuer: 'https://gitlab.example.com', issuer: 'https://gitlab.example.com',
name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
}, },
"label" => "Company Login" # optional label for SAML login button, defaults to "Saml" label: 'Company Login' # optional label for SAML login button, defaults to "Saml"
} }
] ]
``` ```
...@@ -47,34 +84,52 @@ First configure SAML 2.0 support in GitLab, then register the GitLab application ...@@ -47,34 +84,52 @@ First configure SAML 2.0 support in GitLab, then register the GitLab application
For installations from source: For installations from source:
```yaml ```yaml
- { name: 'saml', - {
name: 'saml',
args: { args: {
assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback', assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8', idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
idp_sso_target_url: 'https://login.example.com/idp', idp_sso_target_url: 'https://login.example.com/idp',
issuer: 'https://gitlab.example.com', issuer: 'https://gitlab.example.com',
name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
} } },
label: 'Company Login' # optional label for SAML login button, defaults to "Saml"
}
``` ```
1. Change the value for 'assertion_consumer_service_url' to match the HTTPS endpoint of GitLab (append 'users/auth/saml/callback' to the HTTPS URL of your GitLab installation to generate the correct value). 1. Change the value for `assertion_consumer_service_url` to match the HTTPS endpoint
of GitLab (append `users/auth/saml/callback` to the HTTPS URL of your GitLab
installation to generate the correct value).
1. Change the values of 'idp_cert_fingerprint', 'idp_sso_target_url', 'name_identifier_format' to match your IdP. Check [the omniauth-saml documentation](https://github.com/PracticallyGreen/omniauth-saml) for details on these options. 1. Change the values of `idp_cert_fingerprint`, `idp_sso_target_url`,
`name_identifier_format` to match your IdP. Check
[the omniauth-saml documentation](https://github.com/omniauth/omniauth-saml)
for details on these options.
1. Change the value of 'issuer' to a unique name, which will identify the application to the IdP. 1. Change the value of `issuer` to a unique name, which will identify the application
to the IdP.
1. Restart GitLab for the changes to take effect. 1. Restart GitLab for the changes to take effect.
1. Register the GitLab SP in your SAML 2.0 IdP, using the application name specified in 'issuer'. 1. Register the GitLab SP in your SAML 2.0 IdP, using the application name specified
in `issuer`.
To ease configuration, most IdP accept a metadata URL for the application to provide configuration information to the IdP. To build the metadata URL for GitLab, append 'users/auth/saml/metadata' to the HTTPS URL of your GitLab installation, for instance: To ease configuration, most IdP accept a metadata URL for the application to provide
configuration information to the IdP. To build the metadata URL for GitLab, append
`users/auth/saml/metadata` to the HTTPS URL of your GitLab installation, for instance:
``` ```
https://gitlab.example.com/users/auth/saml/metadata https://gitlab.example.com/users/auth/saml/metadata
``` ```
At a minimum the IdP *must* provide a claim containing the user's email address, using claim name 'email' or 'mail'. The email will be used to automatically generate the GitLab username. GitLab will also use claims with name 'name', 'first_name', 'last_name' (see [the omniauth-saml gem](https://github.com/PracticallyGreen/omniauth-saml/blob/master/lib/omniauth/strategies/saml.rb) for supported claims). At a minimum the IdP *must* provide a claim containing the user's email address, using
claim name `email` or `mail`. The email will be used to automatically generate the GitLab
username. GitLab will also use claims with name `name`, `first_name`, `last_name`
(see [the omniauth-saml gem](https://github.com/omniauth/omniauth-saml/blob/master/lib/omniauth/strategies/saml.rb)
for supported claims).
On the sign in page there should now be a SAML button below the regular sign in form. Click the icon to begin the authentication process. If everything goes well the user will be returned to GitLab and will be signed in. On the sign in page there should now be a SAML button below the regular sign in form.
Click the icon to begin the authentication process. If everything goes well the user
will be returned to GitLab and will be signed in.
## Troubleshooting ## Troubleshooting
...@@ -82,7 +137,7 @@ If you see a "500 error" in GitLab when you are redirected back from the SAML si ...@@ -82,7 +137,7 @@ If you see a "500 error" in GitLab when you are redirected back from the SAML si
this likely indicates that GitLab could not get the email address for the SAML user. this likely indicates that GitLab could not get the email address for the SAML user.
Make sure the IdP provides a claim containing the user's email address, using claim name Make sure the IdP provides a claim containing the user's email address, using claim name
'email' or 'mail'. The email will be used to automatically generate the GitLab username. `email` or `mail`.
If after signing in into your SAML server you are redirected back to the sign in page and If after signing in into your SAML server you are redirected back to the sign in page and
no error is displayed, check your `production.log` file. It will most likely contain the no error is displayed, check your `production.log` file. It will most likely contain the
......
...@@ -32,3 +32,4 @@ ...@@ -32,3 +32,4 @@
- [Merge When Build Succeeds](merge_when_build_succeeds.md) - [Merge When Build Succeeds](merge_when_build_succeeds.md)
- [Manage large binaries with Git LFS](lfs/manage_large_binaries_with_git_lfs.md) - [Manage large binaries with Git LFS](lfs/manage_large_binaries_with_git_lfs.md)
- [Importing from SVN, GitHub, BitBucket, etc](importing/README.md) - [Importing from SVN, GitHub, BitBucket, etc](importing/README.md)
- [Todos](todos.md)
# GitLab ToDos
>**Note:** This feature was [introduced][ce-2817] in GitLab 8.5.
When you log into GitLab, you normally want to see where you should spend your
time and take some action, or what you need to keep an eye on. All without the
mess of a huge pile of e-mail notifications. GitLab is where you do your work,
so being able to get started quickly is very important.
Todos is a chronological list of to-dos that are waiting for your input, all
in a simple dashboard.
![Todos screenshot showing a list of items to check on](img/todos_index.png)
---
You can access quickly your Todos dashboard by clicking the round gray icon
next to the search bar in the upper right corner.
![Todos icon](img/todos_icon.png)
## What triggers a Todo
A Todo appears in your Todos dashboard when:
- an issue or merge request is assigned to you
- you are `@mentioned` in an issue or merge request, be it the description of
the issue/merge request or in a comment
>**Note:** Commenting on a commit will _not_ trigger a Todo.
## How a Todo is marked as Done
Any action to the corresponding issue or merge request will mark your Todo as
**Done**. This action can include:
- changing the assignee
- changing the milestone
- adding/removing a label
- commenting on the issue
In case where you think no action is needed, you can manually mark the todo as
done by clicking the corresponding **Done** button, and it will disappear from
your Todos list. If you want to mark all your Todos as done, just click on the
**Mark all as done** button.
---
In order for a Todo to be marked as done, the action must be coming from you.
So, if you close the related issue or merge the merge request yourself, and you
had a Todo for that, it will automatically get marked as done. On the other
hand, if someone else closes, merges or takes action on the issue or merge
request, your Todo will remain pending. This makes sense because you may need
to give attention to an issue even if it has been resolved.
There is just one Todo per issue or merge request, so mentioning a user a
hundred times in an issue will only trigger one Todo.
## Filtering your Todos
In general, there are four kinds of filters you can use on your Todos
dashboard:
| Filter | Description |
| ------ | ----------- |
| Project | Filter by project |
| Author | Filter by the author that triggered the Todo |
| Type | Filter by issue or merge request |
| Action | Filter by the action that triggered the Todo (Assigned or Mentioned)|
You can choose more than one filters at the same time.
[ce-2817]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2817
...@@ -22,11 +22,23 @@ Feature: Groups ...@@ -22,11 +22,23 @@ Feature: Groups
When I visit group "Owned" issues page When I visit group "Owned" issues page
Then I should see issues from group "Owned" assigned to me Then I should see issues from group "Owned" assigned to me
Scenario: I should not see issues from archived project in "Owned" group issues list
Given Group "Owned" has archived project
And the archived project have some issues
When I visit group "Owned" issues page
Then I should not see issues from the archived project
Scenario: I should see group "Owned" merge requests list Scenario: I should see group "Owned" merge requests list
Given project from group "Owned" has merge requests assigned to me Given project from group "Owned" has merge requests assigned to me
When I visit group "Owned" merge requests page When I visit group "Owned" merge requests page
Then I should see merge requests from group "Owned" assigned to me Then I should see merge requests from group "Owned" assigned to me
Scenario: I should not see merge requests from archived project in "Owned" group merge requests list
Given Group "Owned" has archived project
And the archived project have some merge_requests
When I visit group "Owned" merge requests page
Then I should not see merge requests from the archived project
Scenario: I should see edit group "Owned" page Scenario: I should see edit group "Owned" page
When I visit group "Owned" settings page When I visit group "Owned" settings page
And I change group "Owned" name to "new-name" And I change group "Owned" name to "new-name"
......
...@@ -65,3 +65,27 @@ Feature: Search ...@@ -65,3 +65,27 @@ Feature: Search
And I search for "Wiki content" And I search for "Wiki content"
And I click "Wiki" link And I click "Wiki" link
Then I should see "test_wiki" link in the search results Then I should see "test_wiki" link in the search results
Scenario: I logout and should see project I am looking for
Given project "Shop" is public
And I logout
And I visit dashboard search page
And I search for "Sho"
Then I should see "Shop" project link
Scenario: I logout and should see issues I am looking for
Given project "Shop" is public
And I logout
And project has issues
And I visit dashboard search page
When I search for "Foo"
And I click "Issues" link
Then I should see "Foo" link in the search results
And I should not see "Bar" link in the search results
Scenario: I logout and should see project code I am looking for
Given project "Shop" is public
And I logout
When I visit project "Shop" page
And I search for "rspec" on project page
Then I should see code results for project "Shop"
...@@ -48,6 +48,18 @@ class Spinach::Features::Groups < Spinach::FeatureSteps ...@@ -48,6 +48,18 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
end end
end end
step 'I should not see issues from the archived project' do
@archived_project.issues.each do |issue|
expect(page).not_to have_content issue.title
end
end
step 'I should not see merge requests from the archived project' do
@archived_project.merge_requests.each do |mr|
expect(page).not_to have_content mr.title
end
end
step 'I should see merge requests from group "Owned" assigned to me' do step 'I should see merge requests from group "Owned" assigned to me' do
assigned_to_me(:merge_requests).each do |issue| assigned_to_me(:merge_requests).each do |issue|
expect(page).to have_content issue.title[0..80] expect(page).to have_content issue.title[0..80]
...@@ -159,7 +171,7 @@ class Spinach::Features::Groups < Spinach::FeatureSteps ...@@ -159,7 +171,7 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
step 'Group "Owned" has archived project' do step 'Group "Owned" has archived project' do
group = Group.find_by(name: 'Owned') group = Group.find_by(name: 'Owned')
create(:project, namespace: group, archived: true, path: "archived-project") @archived_project = create(:project, namespace: group, archived: true, path: "archived-project")
end end
step 'I should see "archived" label' do step 'I should see "archived" label' do
...@@ -192,6 +204,21 @@ class Spinach::Features::Groups < Spinach::FeatureSteps ...@@ -192,6 +204,21 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
visit group_path(-1) visit group_path(-1)
end end
step 'the archived project have some issues' do
create :issue,
project: @archived_project,
assignee: current_user,
author: current_user
end
step 'the archived project have some merge requests' do
create :merge_request,
source_project: @archived_project,
target_project: @archived_project,
assignee: current_user,
author: current_user
end
private private
def assigned_to_me(key) def assigned_to_me(key)
......
...@@ -13,7 +13,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps ...@@ -13,7 +13,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
fill_in 'user_website_url', with: 'testurl' fill_in 'user_website_url', with: 'testurl'
fill_in 'user_location', with: 'Ukraine' fill_in 'user_location', with: 'Ukraine'
fill_in 'user_bio', with: 'I <3 GitLab' fill_in 'user_bio', with: 'I <3 GitLab'
click_button 'Save changes' click_button 'Update profile settings'
@user.reload @user.reload
end end
...@@ -237,7 +237,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps ...@@ -237,7 +237,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
page.find('#user_avatar_crop_y', visible: false).set('0') page.find('#user_avatar_crop_y', visible: false).set('0')
page.find('#user_avatar_crop_size', visible: false).set('256') page.find('#user_avatar_crop_size', visible: false).set('256')
click_button "Save changes" click_button "Update profile settings"
@user.reload @user.reload
end end
......
...@@ -18,6 +18,11 @@ class Spinach::Features::Search < Spinach::FeatureSteps ...@@ -18,6 +18,11 @@ class Spinach::Features::Search < Spinach::FeatureSteps
click_button "Search" click_button "Search"
end end
step 'I search for "rspec" on project page' do
fill_in "search", with: "rspec"
click_button "Go"
end
step 'I search for "Wiki content"' do step 'I search for "Wiki content"' do
fill_in "dashboard_search", with: "content" fill_in "dashboard_search", with: "content"
click_button "Search" click_button "Search"
...@@ -103,4 +108,8 @@ class Spinach::Features::Search < Spinach::FeatureSteps ...@@ -103,4 +108,8 @@ class Spinach::Features::Search < Spinach::FeatureSteps
@wiki = ::ProjectWiki.new(project, current_user) @wiki = ::ProjectWiki.new(project, current_user)
@wiki.create_page("test_wiki", "Some Wiki content", :markdown, "first commit") @wiki.create_page("test_wiki", "Some Wiki content", :markdown, "first commit")
end end
step 'project "Shop" is public' do
project.update_attributes(visibility_level: Project::PUBLIC)
end
end end
...@@ -26,4 +26,20 @@ module SharedUser ...@@ -26,4 +26,20 @@ module SharedUser
step 'I have no ssh keys' do step 'I have no ssh keys' do
@user.keys.delete_all @user.keys.delete_all
end end
step 'I click on "Personal projects" tab' do
page.within '.nav-links' do
click_link 'Personal projects'
end
expect(page).to have_css('.tab-content #projects.active')
end
step 'I click on "Contributed projects" tab' do
page.within '.nav-links' do
click_link 'Contributed projects'
end
expect(page).to have_css('.tab-content #contributed.active')
end
end end
...@@ -5,10 +5,12 @@ Feature: User ...@@ -5,10 +5,12 @@ Feature: User
# Signed out # Signed out
@javascript
Scenario: I visit user "John Doe" page while not signed in when he owns a public project Scenario: I visit user "John Doe" page while not signed in when he owns a public project
Given "John Doe" owns internal project "Internal" Given "John Doe" owns internal project "Internal"
And "John Doe" owns public project "Community" And "John Doe" owns public project "Community"
When I visit user "John Doe" page When I visit user "John Doe" page
And I click on "Personal projects" tab
Then I should see user "John Doe" page Then I should see user "John Doe" page
And I should not see project "Enterprise" And I should not see project "Enterprise"
And I should not see project "Internal" And I should not see project "Internal"
...@@ -16,28 +18,34 @@ Feature: User ...@@ -16,28 +18,34 @@ Feature: User
# Signed in as someone else # Signed in as someone else
@javascript
Scenario: I visit user "John Doe" page while signed in as someone else when he owns a public project Scenario: I visit user "John Doe" page while signed in as someone else when he owns a public project
Given "John Doe" owns public project "Community" Given "John Doe" owns public project "Community"
And "John Doe" owns internal project "Internal" And "John Doe" owns internal project "Internal"
And I sign in as a user And I sign in as a user
When I visit user "John Doe" page When I visit user "John Doe" page
And I click on "Personal projects" tab
Then I should see user "John Doe" page Then I should see user "John Doe" page
And I should not see project "Enterprise" And I should not see project "Enterprise"
And I should see project "Internal" And I should see project "Internal"
And I should see project "Community" And I should see project "Community"
@javascript
Scenario: I visit user "John Doe" page while signed in as someone else when he is not authorized to a public project Scenario: I visit user "John Doe" page while signed in as someone else when he is not authorized to a public project
Given "John Doe" owns internal project "Internal" Given "John Doe" owns internal project "Internal"
And I sign in as a user And I sign in as a user
When I visit user "John Doe" page When I visit user "John Doe" page
And I click on "Personal projects" tab
Then I should see user "John Doe" page Then I should see user "John Doe" page
And I should not see project "Enterprise" And I should not see project "Enterprise"
And I should see project "Internal" And I should see project "Internal"
And I should not see project "Community" And I should not see project "Community"
@javascript
Scenario: I visit user "John Doe" page while signed in as someone else when he is not authorized to a project I can see Scenario: I visit user "John Doe" page while signed in as someone else when he is not authorized to a project I can see
Given I sign in as a user Given I sign in as a user
When I visit user "John Doe" page When I visit user "John Doe" page
And I click on "Personal projects" tab
Then I should see user "John Doe" page Then I should see user "John Doe" page
And I should not see project "Enterprise" And I should not see project "Enterprise"
And I should not see project "Internal" And I should not see project "Internal"
...@@ -45,19 +53,23 @@ Feature: User ...@@ -45,19 +53,23 @@ Feature: User
# Signed in as the user himself # Signed in as the user himself
@javascript
Scenario: I visit user "John Doe" page while signed in as "John Doe" when he has a public project Scenario: I visit user "John Doe" page while signed in as "John Doe" when he has a public project
Given "John Doe" owns internal project "Internal" Given "John Doe" owns internal project "Internal"
And "John Doe" owns public project "Community" And "John Doe" owns public project "Community"
And I sign in as "John Doe" And I sign in as "John Doe"
When I visit user "John Doe" page When I visit user "John Doe" page
And I click on "Personal projects" tab
Then I should see user "John Doe" page Then I should see user "John Doe" page
And I should see project "Enterprise" And I should see project "Enterprise"
And I should see project "Internal" And I should see project "Internal"
And I should see project "Community" And I should see project "Community"
@javascript
Scenario: I visit user "John Doe" page while signed in as "John Doe" when he has no public project Scenario: I visit user "John Doe" page while signed in as "John Doe" when he has no public project
Given I sign in as "John Doe" Given I sign in as "John Doe"
When I visit user "John Doe" page When I visit user "John Doe" page
And I click on "Personal projects" tab
Then I should see user "John Doe" page Then I should see user "John Doe" page
And I should see project "Enterprise" And I should see project "Enterprise"
And I should not see project "Internal" And I should not see project "Internal"
...@@ -75,6 +87,7 @@ Feature: User ...@@ -75,6 +87,7 @@ Feature: User
Given I sign in as a user Given I sign in as a user
And "John Doe" has contributions And "John Doe" has contributions
When I visit user "John Doe" page When I visit user "John Doe" page
And I click on "Contributed projects" tab
Then I should see user "John Doe" page Then I should see user "John Doe" page
And I should see contributed projects And I should see contributed projects
And I should see contributions calendar And I should see contributions calendar
...@@ -18,10 +18,12 @@ module API ...@@ -18,10 +18,12 @@ module API
# Examples: # Examples:
# GET /projects/:id/repository/commits/:sha/statuses # GET /projects/:id/repository/commits/:sha/statuses
get ':id/repository/commits/:sha/statuses' do get ':id/repository/commits/:sha/statuses' do
authorize! :read_commit_status, user_project authorize!(:read_commit_status, user_project)
sha = params[:sha]
ci_commit = user_project.ci_commit(sha) not_found!('Commit') unless user_project.commit(params[:sha])
not_found! 'Commit' unless ci_commit ci_commit = user_project.ci_commit(params[:sha])
return [] unless ci_commit
statuses = ci_commit.statuses statuses = ci_commit.statuses
statuses = statuses.latest unless parse_boolean(params[:all]) statuses = statuses.latest unless parse_boolean(params[:all])
statuses = statuses.where(ref: params[:ref]) if params[:ref].present? statuses = statuses.where(ref: params[:ref]) if params[:ref].present?
......
...@@ -50,16 +50,24 @@ module Banzai ...@@ -50,16 +50,24 @@ module Banzai
# See https://github.com/gollum/gollum/wiki # See https://github.com/gollum/gollum/wiki
# #
# Rubular: http://rubular.com/r/7dQnE5CUCH # Rubular: http://rubular.com/r/7dQnE5CUCH
TAGS_PATTERN = %r{\[\[(.+?)\]\]} TAGS_PATTERN = %r{\[\[(.+?)\]\]}.freeze
# Pattern to match allowed image extensions # Pattern to match allowed image extensions
ALLOWED_IMAGE_EXTENSIONS = %r{.+(jpg|png|gif|svg|bmp)\z}i ALLOWED_IMAGE_EXTENSIONS = %r{.+(jpg|png|gif|svg|bmp)\z}i.freeze
def call def call
search_text_nodes(doc).each do |node| search_text_nodes(doc).each do |node|
# A Gollum ToC tag is `[[_TOC_]]`, but due to MarkdownFilter running
# before this one, it will be converted into `[[<em>TOC</em>]]`, so it
# needs special-case handling
if toc_tag?(node)
next unless result[:toc].present?
process_toc_tag(node)
else
content = node.content content = node.content
next unless content.match(TAGS_PATTERN) next unless content =~ TAGS_PATTERN
html = process_tag($1) html = process_tag($1)
...@@ -67,12 +75,19 @@ module Banzai ...@@ -67,12 +75,19 @@ module Banzai
node.replace(html) node.replace(html)
end end
end end
end
doc doc
end end
private private
# Replace an entire `[[<em>TOC</em>]]` node with the result generated by
# TableOfContentsFilter
def process_toc_tag(node)
node.parent.parent.replace(result[:toc])
end
# Process a single tag into its final HTML form. # Process a single tag into its final HTML form.
# #
# tag - The String tag contents (the stuff inside the double brackets). # tag - The String tag contents (the stuff inside the double brackets).
...@@ -108,6 +123,12 @@ module Banzai ...@@ -108,6 +123,12 @@ module Banzai
end end
end end
def toc_tag?(node)
node.content == 'TOC' &&
node.parent.name == 'em' &&
node.parent.parent.text == '[[TOC]]'
end
def image?(path) def image?(path)
path =~ ALLOWED_IMAGE_EXTENSIONS path =~ ALLOWED_IMAGE_EXTENSIONS
end end
......
module Banzai
class FilterArray < Array
# Insert a value immediately after another value
#
# If the preceding value does not exist, the new value is added to the end
# of the Array.
def insert_after(after_value, value)
i = index(after_value) || length - 1
insert(i + 1, value)
end
# Insert a value immediately before another value
#
# If the succeeding value does not exist, the new value is added to the
# beginning of the Array.
def insert_before(before_value, value)
i = index(before_value) || -1
if i < 0
unshift(value)
else
insert(i, value)
end
end
end
end
...@@ -4,7 +4,7 @@ module Banzai ...@@ -4,7 +4,7 @@ module Banzai
module Pipeline module Pipeline
class BasePipeline class BasePipeline
def self.filters def self.filters
[] FilterArray[]
end end
def self.transform_context(context) def self.transform_context(context)
......
...@@ -2,7 +2,7 @@ module Banzai ...@@ -2,7 +2,7 @@ module Banzai
module Pipeline module Pipeline
class BroadcastMessagePipeline < DescriptionPipeline class BroadcastMessagePipeline < DescriptionPipeline
def self.filters def self.filters
@filters ||= [ @filters ||= FilterArray[
Filter::MarkdownFilter, Filter::MarkdownFilter,
Filter::SanitizationFilter, Filter::SanitizationFilter,
......
...@@ -10,7 +10,7 @@ module Banzai ...@@ -10,7 +10,7 @@ module Banzai
end end
def self.filters def self.filters
pipelines.flat_map(&:filters) FilterArray.new(pipelines.flat_map(&:filters))
end end
def self.transform_context(context) def self.transform_context(context)
......
...@@ -2,7 +2,7 @@ module Banzai ...@@ -2,7 +2,7 @@ module Banzai
module Pipeline module Pipeline
class GfmPipeline < BasePipeline class GfmPipeline < BasePipeline
def self.filters def self.filters
@filters ||= [ @filters ||= FilterArray[
Filter::SyntaxHighlightFilter, Filter::SyntaxHighlightFilter,
Filter::SanitizationFilter, Filter::SanitizationFilter,
......
...@@ -2,7 +2,7 @@ module Banzai ...@@ -2,7 +2,7 @@ module Banzai
module Pipeline module Pipeline
class PlainMarkdownPipeline < BasePipeline class PlainMarkdownPipeline < BasePipeline
def self.filters def self.filters
[ FilterArray[
Filter::MarkdownFilter Filter::MarkdownFilter
] ]
end end
......
...@@ -2,7 +2,7 @@ module Banzai ...@@ -2,7 +2,7 @@ module Banzai
module Pipeline module Pipeline
class PostProcessPipeline < BasePipeline class PostProcessPipeline < BasePipeline
def self.filters def self.filters
[ FilterArray[
Filter::RelativeLinkFilter, Filter::RelativeLinkFilter,
Filter::RedactorFilter Filter::RedactorFilter
] ]
......
...@@ -2,7 +2,7 @@ module Banzai ...@@ -2,7 +2,7 @@ module Banzai
module Pipeline module Pipeline
class ReferenceExtractionPipeline < BasePipeline class ReferenceExtractionPipeline < BasePipeline
def self.filters def self.filters
[ FilterArray[
Filter::ReferenceGathererFilter Filter::ReferenceGathererFilter
] ]
end end
......
...@@ -2,7 +2,7 @@ module Banzai ...@@ -2,7 +2,7 @@ module Banzai
module Pipeline module Pipeline
class SingleLinePipeline < GfmPipeline class SingleLinePipeline < GfmPipeline
def self.filters def self.filters
@filters ||= [ @filters ||= FilterArray[
Filter::SanitizationFilter, Filter::SanitizationFilter,
Filter::EmojiFilter, Filter::EmojiFilter,
......
...@@ -4,7 +4,8 @@ module Banzai ...@@ -4,7 +4,8 @@ module Banzai
module Pipeline module Pipeline
class WikiPipeline < FullPipeline class WikiPipeline < FullPipeline
def self.filters def self.filters
super.insert(1, Filter::GollumTagsFilter) @filters ||= super.insert_after(Filter::TableOfContentsFilter,
Filter::GollumTagsFilter)
end end
end end
end end
......
...@@ -12,7 +12,7 @@ module Gitlab ...@@ -12,7 +12,7 @@ module Gitlab
end end
def execute def execute
project_identifier = CGI.escape(project.import_source, '/') project_identifier = CGI.escape(project.import_source)
#Issues && Comments #Issues && Comments
issues = client.issues(project_identifier) issues = client.issues(project_identifier)
......
...@@ -729,13 +729,15 @@ namespace :gitlab do ...@@ -729,13 +729,15 @@ namespace :gitlab do
def check_imap_authentication def check_imap_authentication
print "IMAP server credentials are correct? ... " print "IMAP server credentials are correct? ... "
config = Gitlab.config.incoming_email config_path = Rails.root.join('config', 'mail_room.yml')
config_file = YAML.load(ERB.new(File.read(config_path)).result)
config = config_file[:mailboxes].first
if config if config
begin begin
imap = Net::IMAP.new(config.host, port: config.port, ssl: config.ssl) imap = Net::IMAP.new(config[:host], port: config[:port], ssl: config[:ssl])
imap.starttls if config.start_tls imap.starttls if config[:start_tls]
imap.login(config.user, config.password) imap.login(config[:email], config[:password])
connected = true connected = true
rescue rescue
connected = false connected = false
......
require "spec_helper"
describe "mail_room.yml" do
let(:config_path) { "config/mail_room.yml" }
let(:configuration) { YAML.load(ERB.new(File.read(config_path)).result) }
context "when incoming email is disabled" do
before do
ENV["MAIL_ROOM_GITLAB_CONFIG_FILE"] = Rails.root.join("spec/fixtures/mail_room_disabled.yml").to_s
end
after do
ENV["MAIL_ROOM_GITLAB_CONFIG_FILE"] = nil
end
it "contains no configuration" do
expect(configuration[:mailboxes]).to be_nil
end
end
context "when incoming email is enabled" do
before do
ENV["MAIL_ROOM_GITLAB_CONFIG_FILE"] = Rails.root.join("spec/fixtures/mail_room_enabled.yml").to_s
end
after do
ENV["MAIL_ROOM_GITLAB_CONFIG_FILE"] = nil
end
it "contains the intended configuration" do
expect(configuration[:mailboxes].length).to eq(1)
mailbox = configuration[:mailboxes].first
expect(mailbox[:host]).to eq("imap.gmail.com")
expect(mailbox[:port]).to eq(993)
expect(mailbox[:ssl]).to eq(true)
expect(mailbox[:start_tls]).to eq(false)
expect(mailbox[:email]).to eq("gitlab-incoming@gmail.com")
expect(mailbox[:password]).to eq("[REDACTED]")
expect(mailbox[:name]).to eq("inbox")
redis_config_file = Rails.root.join('config', 'resque.yml')
redis_url =
if File.exists?(redis_config_file)
YAML.load_file(redis_config_file)[Rails.env]
else
"redis://localhost:6379"
end
expect(mailbox[:delivery_options][:redis_url]).to eq(redis_url)
expect(mailbox[:arbitration_options][:redis_url]).to eq(redis_url)
end
end
end
require 'spec_helper'
describe Ci::ProjectsController do
let(:visibility) { :public }
let!(:project) { create(:project, visibility, ci_id: 1) }
let(:ci_id) { project.ci_id }
##
# Specs for *deprecated* CI badge
#
describe '#badge' do
shared_examples 'badge provider' do
it 'shows badge' do
expect(response.status).to eq 200
expect(response.headers)
.to include('Content-Type' => 'image/svg+xml')
end
end
context 'user not signed in' do
before { get(:badge, id: ci_id) }
context 'project has no ci_id reference' do
let(:ci_id) { 123 }
it 'returns 404' do
expect(response.status).to eq 404
end
end
context 'project is public' do
let(:visibility) { :public }
it_behaves_like 'badge provider'
end
context 'project is private' do
let(:visibility) { :private }
it_behaves_like 'badge provider'
end
end
context 'user signed in' do
let(:user) { create(:user) }
before { sign_in(user) }
before { get(:badge, id: ci_id) }
context 'private is internal' do
let(:visibility) { :internal }
it_behaves_like 'badge provider'
end
end
end
end
require 'spec_helper'
describe Projects::ForksController do
let(:user) { create(:user) }
let(:project) { create(:project, visibility_level: Project::PUBLIC) }
let(:forked_project) { Projects::ForkService.new(project, user).execute }
let(:group) { create(:group, owner: forked_project.creator) }
describe 'GET index' do
def get_forks
get :index,
namespace_id: project.namespace.to_param,
project_id: project.to_param
end
context 'when fork is public' do
before { forked_project.update_attribute(:visibility_level, Project::PUBLIC) }
it 'should be visible for non logged in users' do
get_forks
expect(assigns[:forks]).to be_present
end
end
context 'when fork is private' do
before do
forked_project.update_attributes(visibility_level: Project::PRIVATE, group: group)
end
it 'should not be visible for non logged in users' do
get_forks
expect(assigns[:forks]).to be_blank
end
context 'when user is logged in' do
before { sign_in(project.creator) }
context 'when user is not a Project member neither a group member' do
it 'should not see the Project listed' do
get_forks
expect(assigns[:forks]).to be_blank
end
end
context 'when user is a member of the Project' do
before { forked_project.team << [project.creator, :developer] }
it 'should see the project listed' do
get_forks
expect(assigns[:forks]).to be_present
end
end
context 'when user is a member of the Group' do
before { forked_project.group.add_developer(project.creator) }
it 'should see the project listed' do
get_forks
expect(assigns[:forks]).to be_present
end
end
end
end
end
end
test:
incoming_email:
enabled: false
address: "gitlab-incoming+%{key}@gmail.com"
user: "gitlab-incoming@gmail.com"
password: "[REDACTED]"
host: "imap.gmail.com"
port: 993
ssl: true
start_tls: false
mailbox: "inbox"
test:
incoming_email:
enabled: true
address: "gitlab-incoming+%{key}@gmail.com"
user: "gitlab-incoming@gmail.com"
password: "[REDACTED]"
host: "imap.gmail.com"
port: 993
ssl: true
start_tls: false
mailbox: "inbox"
%form{ action: '/foo' } %form.js-quick-submit{ action: '/foo' }
%input.js-quick-submit{ type: 'text' } %input{ type: 'text' }
%textarea.js-quick-submit %textarea
%input{ type: 'submit'} Submit %input{ type: 'submit'} Submit
%button.btn{ type: 'submit' } Submit %button.btn{ type: 'submit' } Submit
...@@ -86,4 +86,56 @@ describe Banzai::Filter::GollumTagsFilter, lib: true do ...@@ -86,4 +86,56 @@ describe Banzai::Filter::GollumTagsFilter, lib: true do
expect(doc.at_css('a')['href']).to eq 'wiki-slug' expect(doc.at_css('a')['href']).to eq 'wiki-slug'
end end
end end
context 'table of contents' do
let(:pipeline) { Banzai::Pipeline[:wiki] }
it 'replaces the tag with the TableOfContentsFilter result' do
markdown = <<-MD.strip_heredoc
[[_TOC_]]
## Header
Foo
MD
result = pipeline.call(markdown, project_wiki: project_wiki, project: project)
aggregate_failures do
expect(result[:output].text).not_to include '[[_TOC_]]'
expect(result[:output].text).not_to include '[['
expect(result[:output].to_html).to include(result[:toc])
end
end
it 'is case-sensitive' do
markdown = <<-MD.strip_heredoc
[[_toc_]]
# Header 1
Foo
MD
output = pipeline.to_html(markdown, project_wiki: project_wiki, project: project)
expect(output).to include('[[<em>toc</em>]]')
end
it 'handles an empty pipeline result' do
# No Markdown headers in this doc, so `result[:toc]` will be empty
markdown = <<-MD.strip_heredoc
[[_TOC_]]
Foo
MD
output = pipeline.to_html(markdown, project_wiki: project_wiki, project: project)
aggregate_failures do
expect(output).not_to include('<ul>')
expect(output).to include('[[<em>TOC</em>]]')
end
end
end
end end
require 'spec_helper'
describe Banzai::FilterArray do
describe '#insert_after' do
it 'inserts an element after a provided element' do
filters = described_class.new(%w(a b c))
filters.insert_after('b', '1')
expect(filters).to eq %w(a b 1 c)
end
it 'inserts an element at the end when the provided element does not exist' do
filters = described_class.new(%w(a b c))
filters.insert_after('d', '1')
expect(filters).to eq %w(a b c 1)
end
end
describe '#insert_before' do
it 'inserts an element before a provided element' do
filters = described_class.new(%w(a b c))
filters.insert_before('b', '1')
expect(filters).to eq %w(a 1 b c)
end
it 'inserts an element at the beginning when the provided element does not exist' do
filters = described_class.new(%w(a b c))
filters.insert_before('d', '1')
expect(filters).to eq %w(1 a b c)
end
end
end
...@@ -664,4 +664,67 @@ describe Project, models: true do ...@@ -664,4 +664,67 @@ describe Project, models: true do
it { is_expected.to eq("http://group.example.com/project") } it { is_expected.to eq("http://group.example.com/project") }
end end
end end
describe '#rename_repo' do
let(:project) { create(:project) }
let(:gitlab_shell) { Gitlab::Shell.new }
before do
# Project#gitlab_shell returns a new instance of Gitlab::Shell on every
# call. This makes testing a bit easier.
allow(project).to receive(:gitlab_shell).and_return(gitlab_shell)
end
it 'renames a repository' do
allow(project).to receive(:previous_changes).and_return('path' => ['foo'])
ns = project.namespace_dir
expect(gitlab_shell).to receive(:mv_repository).
ordered.
with("#{ns}/foo", "#{ns}/#{project.path}").
and_return(true)
expect(gitlab_shell).to receive(:mv_repository).
ordered.
with("#{ns}/foo.wiki", "#{ns}/#{project.path}.wiki").
and_return(true)
expect_any_instance_of(SystemHooksService).
to receive(:execute_hooks_for).
with(project, :rename)
expect_any_instance_of(Gitlab::UploadsTransfer).
to receive(:rename_project).
with('foo', project.path, ns)
expect(project).to receive(:expire_caches_before_rename)
project.rename_repo
end
end
describe '#expire_caches_before_rename' do
let(:project) { create(:project) }
let(:repo) { double(:repo, exists?: true) }
let(:wiki) { double(:wiki, exists?: true) }
it 'expires the caches of the repository and wiki' do
allow(Repository).to receive(:new).
with('foo', project).
and_return(repo)
allow(Repository).to receive(:new).
with('foo.wiki', project).
and_return(wiki)
expect(repo).to receive(:expire_cache)
expect(repo).to receive(:expire_emptiness_caches)
expect(wiki).to receive(:expire_cache)
expect(wiki).to receive(:expire_emptiness_caches)
project.expire_caches_before_rename('foo')
end
end
end end
...@@ -457,13 +457,40 @@ describe Repository, models: true do ...@@ -457,13 +457,40 @@ describe Repository, models: true do
end end
end end
describe '#revert_merge' do describe '#revert' do
let(:new_image_commit) { repository.commit('33f3729a45c02fc67d00adb1b8bca394b0e761d9') }
let(:update_image_commit) { repository.commit('2f63565e7aac07bcdadb654e253078b727143ec4') }
context 'when there is a conflict' do
it 'should abort the operation' do
expect(repository.revert(user, new_image_commit, 'master')).to eq(false)
end
end
context 'when commit was already reverted' do
it 'should abort the operation' do
repository.revert(user, update_image_commit, 'master')
expect(repository.revert(user, update_image_commit, 'master')).to eq(false)
end
end
context 'when commit can be reverted' do
it 'should revert the changes' do it 'should revert the changes' do
repository.revert(user, merge_commit, 'master') expect(repository.revert(user, update_image_commit, 'master')).to be_truthy
end
end
context 'reverting a merge commit' do
it 'should revert the changes' do
merge_commit
expect(repository.blob_at_branch('master', 'files/ruby/feature.rb')).to be_present
repository.revert(user, merge_commit, 'master')
expect(repository.blob_at_branch('master', 'files/ruby/feature.rb')).not_to be_present expect(repository.blob_at_branch('master', 'files/ruby/feature.rb')).not_to be_present
end end
end end
end
describe '#before_delete' do describe '#before_delete' do
describe 'when a repository does not exist' do describe 'when a repository does not exist' do
......
...@@ -176,7 +176,7 @@ describe User, models: true do ...@@ -176,7 +176,7 @@ describe User, models: true do
end end
describe 'avatar' do describe 'avatar' do
it 'only validates when avatar is present' do it 'only validates when avatar is present and changed' do
user = build(:user, :with_avatar) user = build(:user, :with_avatar)
user.avatar_crop_x = nil user.avatar_crop_x = nil
...@@ -184,6 +184,20 @@ describe User, models: true do ...@@ -184,6 +184,20 @@ describe User, models: true do
user.avatar_crop_size = nil user.avatar_crop_size = nil
expect(user).not_to be_valid expect(user).not_to be_valid
expect(user.errors.keys).
to match_array %i(avatar_crop_x avatar_crop_y avatar_crop_size)
end
it 'does not validate when avatar has not changed' do
user = create(:user, :with_avatar)
expect { user.avatar_crop_x = nil }.not_to change(user, :valid?)
end
it 'does not validate when avatar is not present' do
user = create(:user)
expect { user.avatar_crop_y = nil }.not_to change(user, :valid?)
end end
end end
end end
......
...@@ -2,88 +2,125 @@ require 'spec_helper' ...@@ -2,88 +2,125 @@ require 'spec_helper'
describe API::CommitStatus, api: true do describe API::CommitStatus, api: true do
include ApiHelpers include ApiHelpers
let!(:project) { create(:project) } let!(:project) { create(:project) }
let(:commit) { project.repository.commit } let(:commit) { project.repository.commit }
let!(:ci_commit) { project.ensure_ci_commit(commit.id) }
let(:commit_status) { create(:commit_status, commit: ci_commit) } let(:commit_status) { create(:commit_status, commit: ci_commit) }
let(:guest) { create_user(ProjectMember::GUEST) } let(:guest) { create_user(ProjectMember::GUEST) }
let(:reporter) { create_user(ProjectMember::REPORTER) } let(:reporter) { create_user(ProjectMember::REPORTER) }
let(:developer) { create_user(ProjectMember::DEVELOPER) } let(:developer) { create_user(ProjectMember::DEVELOPER) }
let(:sha) { commit.id }
describe "GET /projects/:id/repository/commits/:sha/statuses" do describe "GET /projects/:id/repository/commits/:sha/statuses" do
let(:get_url) { "/projects/#{project.id}/repository/commits/#{sha}/statuses" }
context 'ci commit exists' do
let!(:ci_commit) { project.ensure_ci_commit(commit.id) }
it_behaves_like 'a paginated resources' do it_behaves_like 'a paginated resources' do
let(:request) { get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses", reporter) } let(:request) { get api(get_url, reporter) }
end end
context "reporter user" do context "reporter user" do
let(:statuses_id) { json_response.map { |status| status['id'] } } let(:statuses_id) { json_response.map { |status| status['id'] } }
before do def create_status(opts = {})
@status1 = create(:commit_status, commit: ci_commit, status: 'running') create(:commit_status, { commit: ci_commit }.merge(opts))
@status2 = create(:commit_status, commit: ci_commit, name: 'coverage', status: 'pending')
@status3 = create(:commit_status, commit: ci_commit, name: 'coverage', ref: 'develop', status: 'running', allow_failure: true)
@status4 = create(:commit_status, commit: ci_commit, name: 'coverage', status: 'success')
@status5 = create(:commit_status, commit: ci_commit, ref: 'develop', status: 'success')
@status6 = create(:commit_status, commit: ci_commit, status: 'success')
end end
it "should return latest commit statuses" do let!(:status1) { create_status(status: 'running') }
get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses", reporter) let!(:status2) { create_status(name: 'coverage', status: 'pending') }
let!(:status3) { create_status(ref: 'develop', status: 'running', allow_failure: true) }
let!(:status4) { create_status(name: 'coverage', status: 'success') }
let!(:status5) { create_status(name: 'coverage', ref: 'develop', status: 'success') }
let!(:status6) { create_status(status: 'success') }
context 'latest commit statuses' do
before { get api(get_url, reporter) }
it 'returns latest commit statuses' do
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(json_response).to be_an Array expect(json_response).to be_an Array
expect(statuses_id).to contain_exactly(@status3.id, @status4.id, @status5.id, @status6.id) expect(statuses_id).to contain_exactly(status3.id, status4.id, status5.id, status6.id)
json_response.sort_by!{ |status| status['id'] } json_response.sort_by!{ |status| status['id'] }
expect(json_response.map{ |status| status['allow_failure'] }).to eq([true, false, false, false]) expect(json_response.map{ |status| status['allow_failure'] }).to eq([true, false, false, false])
end end
end
it "should return all commit statuses" do context 'all commit statuses' do
get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses?all=1", reporter) before { get api(get_url, reporter), all: 1 }
it 'returns all commit statuses' do
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(json_response).to be_an Array expect(json_response).to be_an Array
expect(statuses_id).to contain_exactly(@status1.id, @status2.id, @status3.id, @status4.id, @status5.id, @status6.id) expect(statuses_id).to contain_exactly(status1.id, status2.id,
status3.id, status4.id,
status5.id, status6.id)
end
end end
it "should return latest commit statuses for specific ref" do context 'latest commit statuses for specific ref' do
get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses?ref=develop", reporter) before { get api(get_url, reporter), ref: 'develop' }
it 'returns latest commit statuses for specific ref' do
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(json_response).to be_an Array expect(json_response).to be_an Array
expect(statuses_id).to contain_exactly(@status3.id, @status5.id) expect(statuses_id).to contain_exactly(status3.id, status5.id)
end end
end
context 'latest commit statues for specific name' do
before { get api(get_url, reporter), name: 'coverage' }
it "should return latest commit statuses for specific name" do it 'return latest commit statuses for specific name' do
get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses?name=coverage", reporter)
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(json_response).to be_an Array expect(json_response).to be_an Array
expect(statuses_id).to contain_exactly(@status3.id, @status4.id) expect(statuses_id).to contain_exactly(status4.id, status5.id)
end
end
end
end
context 'ci commit does not exist' do
before { get api(get_url, reporter) }
it 'returns empty array' do
expect(response.status).to eq 200
expect(json_response).to be_an Array
expect(json_response).to be_empty
end end
end end
context "guest user" do context "guest user" do
before { get api(get_url, guest) }
it "should not return project commits" do it "should not return project commits" do
get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses", guest)
expect(response.status).to eq(403) expect(response.status).to eq(403)
end end
end end
context "unauthorized user" do context "unauthorized user" do
before { get api(get_url) }
it "should not return project commits" do it "should not return project commits" do
get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses")
expect(response.status).to eq(401) expect(response.status).to eq(401)
end end
end end
end end
describe 'POST /projects/:id/statuses/:sha' do describe 'POST /projects/:id/statuses/:sha' do
let(:post_url) { "/projects/#{project.id}/statuses/#{commit.id}" } let(:post_url) { "/projects/#{project.id}/statuses/#{sha}" }
context 'developer user' do context 'developer user' do
context 'should create commit status' do context 'only required parameters' do
it 'with only required parameters' do before { post api(post_url, developer), state: 'success' }
post api(post_url, developer), state: 'success'
it 'creates commit status' do
expect(response.status).to eq(201) expect(response.status).to eq(201)
expect(json_response['sha']).to eq(commit.id) expect(json_response['sha']).to eq(commit.id)
expect(json_response['status']).to eq('success') expect(json_response['status']).to eq('success')
...@@ -92,9 +129,17 @@ describe API::CommitStatus, api: true do ...@@ -92,9 +129,17 @@ describe API::CommitStatus, api: true do
expect(json_response['target_url']).to be_nil expect(json_response['target_url']).to be_nil
expect(json_response['description']).to be_nil expect(json_response['description']).to be_nil
end end
end
it 'with all optional parameters' do context 'with all optional parameters' do
post api(post_url, developer), state: 'success', context: 'coverage', ref: 'develop', target_url: 'url', description: 'test' before do
optional_params = { state: 'success', context: 'coverage',
ref: 'develop', target_url: 'url', description: 'test' }
post api(post_url, developer), optional_params
end
it 'creates commit status' do
expect(response.status).to eq(201) expect(response.status).to eq(201)
expect(json_response['sha']).to eq(commit.id) expect(json_response['sha']).to eq(commit.id)
expect(json_response['status']).to eq('success') expect(json_response['status']).to eq('success')
...@@ -105,41 +150,52 @@ describe API::CommitStatus, api: true do ...@@ -105,41 +150,52 @@ describe API::CommitStatus, api: true do
end end
end end
context 'should not create commit status' do context 'invalid status' do
it 'with invalid state' do before { post api(post_url, developer), state: 'invalid' }
post api(post_url, developer), state: 'invalid'
it 'does not create commit status' do
expect(response.status).to eq(400) expect(response.status).to eq(400)
end end
end
it 'without state' do context 'request without state' do
post api(post_url, developer) before { post api(post_url, developer) }
it 'does not create commit status' do
expect(response.status).to eq(400) expect(response.status).to eq(400)
end end
end
it 'invalid commit' do context 'invalid commit' do
post api("/projects/#{project.id}/statuses/invalid_sha", developer), state: 'running' let(:sha) { 'invalid_sha' }
before { post api(post_url, developer), state: 'running' }
it 'returns not found error' do
expect(response.status).to eq(404) expect(response.status).to eq(404)
end end
end end
end end
context 'reporter user' do context 'reporter user' do
before { post api(post_url, reporter) }
it 'should not create commit status' do it 'should not create commit status' do
post api(post_url, reporter)
expect(response.status).to eq(403) expect(response.status).to eq(403)
end end
end end
context 'guest user' do context 'guest user' do
before { post api(post_url, guest) }
it 'should not create commit status' do it 'should not create commit status' do
post api(post_url, guest)
expect(response.status).to eq(403) expect(response.status).to eq(403)
end end
end end
context 'unauthorized user' do context 'unauthorized user' do
before { post api(post_url) }
it 'should not create commit status' do it 'should not create commit status' do
post api(post_url)
expect(response.status).to eq(401) expect(response.status).to eq(401)
end end
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