Commit 177c89b8 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge branch 'ce-changes' into 'master'

CE changes from upstream

See merge request !266
parents d991e0b2 8eb29cd5
...@@ -7,6 +7,7 @@ v 7.6.0 ...@@ -7,6 +7,7 @@ v 7.6.0
- Add optional Sidekiq MemoryKiller middleware (enabled via SIDEKIQ_MAX_RSS env variable) - Add optional Sidekiq MemoryKiller middleware (enabled via SIDEKIQ_MAX_RSS env variable)
- -
- -
- Monokai highlighting style now more faithful to original design (Mark Riedesel)
- Create project with repository in synchrony - Create project with repository in synchrony
- Added ability to create empty repo or import existing one if project does not have repository - Added ability to create empty repo or import existing one if project does not have repository
- -
...@@ -16,10 +17,10 @@ v 7.6.0 ...@@ -16,10 +17,10 @@ v 7.6.0
- -
- Change maximum avatar file size from 100KB to 200KB - Change maximum avatar file size from 100KB to 200KB
- -
- - Enable Markdown preview for issues, merge requests, milestones, and notes (Vinnie Okada)
- In the docker directory is a container template based on the Omnibus packages. - In the docker directory is a container template based on the Omnibus packages.
- Update Sidekiq to version 2.17.8 - Update Sidekiq to version 2.17.8
- - Add author filter to project issues and merge requests pages
- Atom feed for user activity - Atom feed for user activity
v 7.5.2 v 7.5.2
......
...@@ -37,7 +37,7 @@ Please send a merge request with a tested solution or a merge request with a fai ...@@ -37,7 +37,7 @@ Please send a merge request with a tested solution or a merge request with a fai
**[Search the issues](https://gitlab.com/gitlab-org/gitlab-ce/issues)** for similar entries before submitting your own, there's a good chance somebody else had the same issue. Show your support with `:+1:` and/or join the discussion. Please submit issues in the following format (as the first post): **[Search the issues](https://gitlab.com/gitlab-org/gitlab-ce/issues)** for similar entries before submitting your own, there's a good chance somebody else had the same issue. Show your support with `:+1:` and/or join the discussion. Please submit issues in the following format (as the first post):
1. **Summary:** Summarize your issue in one sentence (what goes wrong, what did you expect to happen) 1. **Summary:** Summarize your issue in one sentence (what goes wrong, what did you expect to happen)
1. **Steps to reproduce:** How can we reproduce the issue, preferably on the [GitLab development virtual machine with vagrant](https://gitlab.com/gitlab-org/cookbook-gitlab/blob/master/doc/development.md) (start your issue with: `vagrant destroy && vagrant up && vagrant ssh`) 1. **Steps to reproduce:** How can we reproduce the issue
1. **Expected behavior:** Describe your issue in detail 1. **Expected behavior:** Describe your issue in detail
1. **Observed behavior** 1. **Observed behavior**
1. **Relevant logs and/or screenshots:** Please use code blocks (\`\`\`) to format console output, logs, and code as it's very hard to read otherwise. 1. **Relevant logs and/or screenshots:** Please use code blocks (\`\`\`) to format console output, logs, and code as it's very hard to read otherwise.
...@@ -75,6 +75,7 @@ If you can, please submit a merge request with the fix or improvements including ...@@ -75,6 +75,7 @@ If you can, please submit a merge request with the fix or improvements including
1. Link relevant [issues](https://gitlab.com/gitlab-org/gitlab-ce/issues) and/or [feature requests](http://feedback.gitlab.com/) from the merge request description and leave a comment on them with a link back to the MR 1. Link relevant [issues](https://gitlab.com/gitlab-org/gitlab-ce/issues) and/or [feature requests](http://feedback.gitlab.com/) from the merge request description and leave a comment on them with a link back to the MR
1. Be prepared to answer questions and incorporate feedback even if requests for this arrive weeks or months after your MR submission 1. Be prepared to answer questions and incorporate feedback even if requests for this arrive weeks or months after your MR submission
1. If your MR touches code that executes shell commands, make sure it adheres to the [shell command guidelines]( doc/development/shell_commands.md). 1. If your MR touches code that executes shell commands, make sure it adheres to the [shell command guidelines]( doc/development/shell_commands.md).
1. Also have a look at the [shell command guidelines](doc/development/shell_commands.md) if your code reads or opens files, or handles paths to files on disk.
The **official merge window** is in the beginning of the month from the 1st to the 7th day of the month. The best time to submit a MR and get feedback fast. Before this time the GitLab B.V. team is still dealing with work that is created by the monthly release such as assisting subscribers with upgrade issues, the release of Enterprise Edition and the upgrade of GitLab Cloud. After the 7th it is already getting closer to the release date of the next version. This means there is less time to fix the issues created by merging large new features. The **official merge window** is in the beginning of the month from the 1st to the 7th day of the month. The best time to submit a MR and get feedback fast. Before this time the GitLab B.V. team is still dealing with work that is created by the monthly release such as assisting subscribers with upgrade issues, the release of Enterprise Edition and the upgrade of GitLab Cloud. After the 7th it is already getting closer to the release date of the next version. This means there is less time to fix the issues created by merging large new features.
......
...@@ -155,4 +155,4 @@ Please see [Getting help for GitLab](https://about.gitlab.com/getting-help/) on ...@@ -155,4 +155,4 @@ Please see [Getting help for GitLab](https://about.gitlab.com/getting-help/) on
## Is it awesome? ## Is it awesome?
Thanks for [asking this question](https://twitter.com/supersloth/status/489462789384056832) Joshua. Thanks for [asking this question](https://twitter.com/supersloth/status/489462789384056832) Joshua.
[These people](https://twitter.com/gitlabhq/favorites) seem to like it. [These people](https://twitter.com/gitlab/favorites) seem to like it.
...@@ -24,6 +24,51 @@ $(document).ready -> ...@@ -24,6 +24,51 @@ $(document).ready ->
"opacity": 0 "opacity": 0
"display": "none" "display": "none"
# Preview button
$(document).off "click", ".js-md-preview-button"
$(document).on "click", ".js-md-preview-button", (e) ->
###
Shows the Markdown preview.
Lets the server render GFM into Html and displays it.
###
e.preventDefault()
form = $(this).closest("form")
# toggle tabs
form.find(".js-md-write-button").parent().removeClass "active"
form.find(".js-md-preview-button").parent().addClass "active"
# toggle content
form.find(".md-write-holder").hide()
form.find(".md-preview-holder").show()
preview = form.find(".js-md-preview")
mdText = form.find(".markdown-area").val()
if mdText.trim().length is 0
preview.text "Nothing to preview."
else
preview.text "Loading..."
$.get($(this).data("url"),
md_text: mdText
).success (previewData) ->
preview.html previewData
# Write button
$(document).off "click", ".js-md-write-button"
$(document).on "click", ".js-md-write-button", (e) ->
###
Shows the Markdown textarea.
###
e.preventDefault()
form = $(this).closest("form")
# toggle tabs
form.find(".js-md-write-button").parent().addClass "active"
form.find(".js-md-preview-button").parent().removeClass "active"
# toggle content
form.find(".md-write-holder").show()
form.find(".md-preview-holder").hide()
dropzone = $(".div-dropzone").dropzone( dropzone = $(".div-dropzone").dropzone(
url: project_image_path_upload url: project_image_path_upload
dictDefaultMessage: "" dictDefaultMessage: ""
......
...@@ -36,12 +36,6 @@ class @Notes ...@@ -36,12 +36,6 @@ class @Notes
# delete note attachment # delete note attachment
$(document).on "click", ".js-note-attachment-delete", @removeAttachment $(document).on "click", ".js-note-attachment-delete", @removeAttachment
# Preview button
$(document).on "click", ".js-note-preview-button", @previewNote
# Preview button
$(document).on "click", ".js-note-write-button", @writeNote
# reset main target form after submit # reset main target form after submit
$(document).on "ajax:complete", ".js-main-target-form", @resetMainTargetForm $(document).on "ajax:complete", ".js-main-target-form", @resetMainTargetForm
...@@ -77,8 +71,6 @@ class @Notes ...@@ -77,8 +71,6 @@ class @Notes
$(document).off "click", ".note-edit-cancel" $(document).off "click", ".note-edit-cancel"
$(document).off "click", ".js-note-delete" $(document).off "click", ".js-note-delete"
$(document).off "click", ".js-note-attachment-delete" $(document).off "click", ".js-note-attachment-delete"
$(document).off "click", ".js-note-preview-button"
$(document).off "click", ".js-note-write-button"
$(document).off "ajax:complete", ".js-main-target-form" $(document).off "ajax:complete", ".js-main-target-form"
$(document).off "click", ".js-choose-note-attachment-button" $(document).off "click", ".js-choose-note-attachment-button"
$(document).off "click", ".js-discussion-reply-button" $(document).off "click", ".js-discussion-reply-button"
...@@ -165,47 +157,6 @@ class @Notes ...@@ -165,47 +157,6 @@ class @Notes
# cleanup after successfully creating a diff/discussion note # cleanup after successfully creating a diff/discussion note
@removeDiscussionNoteForm(form) @removeDiscussionNoteForm(form)
###
Shows write note textarea.
###
writeNote: (e) ->
e.preventDefault()
form = $(this).closest("form")
# toggle tabs
form.find(".js-note-write-button").parent().addClass "active"
form.find(".js-note-preview-button").parent().removeClass "active"
# toggle content
form.find(".note-write-holder").show()
form.find(".note-preview-holder").hide()
###
Shows the note preview.
Lets the server render GFM into Html and displays it.
###
previewNote: (e) ->
e.preventDefault()
form = $(this).closest("form")
# toggle tabs
form.find(".js-note-write-button").parent().removeClass "active"
form.find(".js-note-preview-button").parent().addClass "active"
# toggle content
form.find(".note-write-holder").hide()
form.find(".note-preview-holder").show()
preview = form.find(".js-note-preview")
noteText = form.find(".js-note-text").val()
if noteText.trim().length is 0
preview.text "Nothing to preview."
else
preview.text "Loading..."
$.post($(this).data("url"),
note: noteText
).success (previewData) ->
preview.html previewData
### ###
Called in response the main target form has been successfully submitted. Called in response the main target form has been successfully submitted.
...@@ -220,7 +171,7 @@ class @Notes ...@@ -220,7 +171,7 @@ class @Notes
form.find(".js-errors").remove() form.find(".js-errors").remove()
# reset text and preview # reset text and preview
form.find(".js-note-write-button").click() form.find(".js-md-write-button").click()
form.find(".js-note-text").val("").trigger "input" form.find(".js-note-text").val("").trigger "input"
### ###
...@@ -270,8 +221,8 @@ class @Notes ...@@ -270,8 +221,8 @@ class @Notes
form.removeClass "js-new-note-form" form.removeClass "js-new-note-form"
# setup preview buttons # setup preview buttons
form.find(".js-note-write-button, .js-note-preview-button").tooltip placement: "left" form.find(".js-md-write-button, .js-md-preview-button").tooltip placement: "left"
previewButton = form.find(".js-note-preview-button") previewButton = form.find(".js-md-preview-button")
form.find(".js-note-text").on "input", -> form.find(".js-note-text").on "input", ->
if $(this).val().trim() isnt "" if $(this).val().trim() isnt ""
previewButton.removeClass("turn-off").addClass "turn-on" previewButton.removeClass("turn-off").addClass "turn-on"
......
...@@ -20,6 +20,7 @@ ...@@ -20,6 +20,7 @@
opacity: 0; opacity: 0;
font-size: 50px; font-size: 50px;
transition: opacity 200ms ease-in-out; transition: opacity 200ms ease-in-out;
pointer-events: none;
} }
.div-dropzone-spinner { .div-dropzone-spinner {
...@@ -50,3 +51,28 @@ ...@@ -50,3 +51,28 @@
margin-bottom: 0; margin-bottom: 0;
transition: opacity 200ms ease-in-out; transition: opacity 200ms ease-in-out;
} }
.md-preview-holder {
background: #FFF;
border: 1px solid #ddd;
min-height: 100px;
padding: 5px;
font-size: 14px;
box-shadow: none;
}
.new_note,
.edit_note,
.issuable-description,
.milestone-description,
.merge-request-form {
.nav-tabs {
margin-bottom: 0;
border: none;
li a,
li.active a {
border: 1px solid #DDD;
}
}
}
...@@ -29,28 +29,30 @@ ...@@ -29,28 +29,30 @@
.hljs-tag, .hljs-tag,
.hljs-tag .hljs-title, .hljs-tag .hljs-title,
.hljs-keyword,
.hljs-literal,
.hljs-strong, .hljs-strong,
.hljs-change, .hljs-change,
.hljs-winutils, .hljs-winutils,
.hljs-flow, .hljs-flow,
.lisp .hljs-title, .lisp .hljs-title,
.clojure .hljs-built_in, .clojure .hljs-built_in,
.hljs-keyword,
.nginx .hljs-title, .nginx .hljs-title,
.tex .hljs-special { .tex .hljs-special {
color: #F92672; color: #F92672;
} }
.hljs { .hljs {
color: #DDD; color: #F8F8F2;
} }
.hljs .hljs-constant, .asciidoc .hljs-code,
.asciidoc .hljs-code { .markdown .hljs-code,
.hljs-literal,
.hljs-function .hljs-keyword {
color: #66D9EF; color: #66D9EF;
} }
.hljs-code, .hljs-code,
.hljs-class .hljs-title, .hljs-class .hljs-title,
.hljs-header { .hljs-header {
...@@ -62,18 +64,27 @@ ...@@ -62,18 +64,27 @@
.hljs-symbol, .hljs-symbol,
.hljs-symbol .hljs-string, .hljs-symbol .hljs-string,
.hljs-value, .hljs-value,
.hljs-constant,
.hljs-number,
.hljs-regexp { .hljs-regexp {
color: #BF79DB; color: #AE81FF;
}
.hljs-string {
color: #E6DB74;
}
.hljs-params {
color: #fd971f;
} }
.hljs-link_url, .hljs-link_url,
.hljs-tag .hljs-value, .hljs-tag .hljs-value,
.hljs-string,
.hljs-bullet, .hljs-bullet,
.hljs-subst, .hljs-subst,
.hljs-title, .hljs-title,
.hljs-emphasis, .hljs-emphasis,
.haskell .hljs-type, .hljs-type,
.hljs-preprocessor, .hljs-preprocessor,
.hljs-pragma, .hljs-pragma,
.ruby .hljs-class .hljs-parent, .ruby .hljs-class .hljs-parent,
...@@ -99,12 +110,12 @@ ...@@ -99,12 +110,12 @@
} }
.hljs-comment, .hljs-comment,
.java .hljs-annotation, .hljs-annotation,
.smartquote, .smartquote,
.hljs-blockquote, .hljs-blockquote,
.hljs-horizontal_rule, .hljs-horizontal_rule,
.python .hljs-decorator,
.hljs-template_comment, .hljs-template_comment,
.hljs-decorator,
.hljs-pi, .hljs-pi,
.hljs-doctype, .hljs-doctype,
.hljs-deletion, .hljs-deletion,
......
...@@ -47,7 +47,7 @@ ...@@ -47,7 +47,7 @@
.event-title { .event-title {
@include str-truncated(72%); @include str-truncated(72%);
color: #333; color: #333;
font-weight: normal; font-weight: 500;
font-size: 14px; font-size: 14px;
.author_name { .author_name {
color: #333; color: #333;
...@@ -56,12 +56,9 @@ ...@@ -56,12 +56,9 @@
.event-body { .event-body {
margin-left: 35px; margin-left: 35px;
margin-right: 100px; margin-right: 100px;
color: #777;
.event-info {
color: #666;
}
.event-note { .event-note {
color: #666;
margin-top: 5px; margin-top: 5px;
.md { .md {
...@@ -72,7 +69,7 @@ ...@@ -72,7 +69,7 @@
border: none; border: none;
background: #f9f9f9; background: #f9f9f9;
border-radius: 0; border-radius: 0;
color: #666; color: #777;
margin: 0 20px; margin: 0 20px;
} }
...@@ -120,7 +117,6 @@ ...@@ -120,7 +117,6 @@
padding: 3px; padding: 3px;
padding-left: 0; padding-left: 0;
border: none; border: none;
color: #666;
.commit-row-title { .commit-row-title {
font-size: 12px; font-size: 12px;
} }
......
...@@ -227,7 +227,6 @@ ul.notes { ...@@ -227,7 +227,6 @@ ul.notes {
margin-bottom: 0; margin-bottom: 0;
} }
.note-preview-holder,
.note_text { .note_text {
background: #FFF; background: #FFF;
border: 1px solid #ddd; border: 1px solid #ddd;
...@@ -246,15 +245,6 @@ ul.notes { ...@@ -246,15 +245,6 @@ ul.notes {
.note_text { .note_text {
width: 100%; width: 100%;
} }
.nav-tabs {
margin-bottom: 0;
border: none;
li a,
li.active a {
border: 1px solid #DDD;
}
}
} }
/* loading indicator */ /* loading indicator */
......
...@@ -29,4 +29,31 @@ class Projects::ApplicationController < ApplicationController ...@@ -29,4 +29,31 @@ class Projects::ApplicationController < ApplicationController
redirect_to project_tree_path(@project, @ref), notice: "This action is not allowed unless you are on top of a branch" redirect_to project_tree_path(@project, @ref), notice: "This action is not allowed unless you are on top of a branch"
end end
end end
def set_filter_variables(collection)
params[:sort] ||= 'newest'
params[:scope] = 'all' if params[:scope].blank?
params[:state] = 'opened' if params[:state].blank?
@sort = params[:sort].humanize
assignee_id = params[:assignee_id]
author_id = params[:author_id]
milestone_id = params[:milestone_id]
if assignee_id.present? && !assignee_id.to_i.zero?
@assignee = @project.team.find(assignee_id)
end
if author_id.present? && !author_id.to_i.zero?
@author = @project.team.find(assignee_id)
end
if milestone_id.present? && !milestone_id.to_i.zero?
@milestone = @project.milestones.find(milestone_id)
end
@assignees = User.where(id: collection.pluck(:assignee_id))
@authors = User.where(id: collection.pluck(:author_id))
end
end end
...@@ -18,18 +18,12 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -18,18 +18,12 @@ class Projects::IssuesController < Projects::ApplicationController
def index def index
terms = params['issue_search'] terms = params['issue_search']
set_filter_variables(@project.issues)
@issues = issues_filtered @issues = IssuesFinder.new.execute(current_user, params.merge(project_id: @project.id))
@issues = @issues.full_search(terms) if terms.present? @issues = @issues.full_search(terms) if terms.present?
@issues = @issues.page(params[:page]).per(20) @issues = @issues.page(params[:page]).per(20)
assignee_id, milestone_id = params[:assignee_id], params[:milestone_id]
@assignee = @project.team.find(assignee_id) if assignee_id.present? && !assignee_id.to_i.zero?
@milestone = @project.milestones.find(milestone_id) if milestone_id.present? && !milestone_id.to_i.zero?
sort_param = params[:sort] || 'newest'
@sort = sort_param.humanize unless sort_param.empty?
@assignees = User.where(id: @project.issues.pluck(:assignee_id)).active
respond_to do |format| respond_to do |format|
format.html format.html
format.atom { render layout: false } format.atom { render layout: false }
...@@ -127,12 +121,6 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -127,12 +121,6 @@ class Projects::IssuesController < Projects::ApplicationController
return render_404 unless @project.issues_enabled return render_404 unless @project.issues_enabled
end end
def issues_filtered
params[:scope] = 'all' if params[:scope].blank?
params[:state] = 'opened' if params[:state].blank?
@issues = IssuesFinder.new.execute(current_user, params.merge(project_id: @project.id))
end
# Since iids are implemented only in 6.1 # Since iids are implemented only in 6.1
# user may navigate to issue page using old global ids. # user may navigate to issue page using old global ids.
# #
......
...@@ -17,18 +17,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -17,18 +17,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_filter :authorize_modify_merge_request!, only: [:close, :edit, :update, :sort] before_filter :authorize_modify_merge_request!, only: [:close, :edit, :update, :sort]
def index def index
params[:sort] ||= 'newest' set_filter_variables(@project.merge_requests)
params[:scope] = 'all' if params[:scope].blank?
params[:state] = 'opened' if params[:state].blank?
@merge_requests = MergeRequestsFinder.new.execute(current_user, params.merge(project_id: @project.id)) @merge_requests = MergeRequestsFinder.new.execute(current_user, params.merge(project_id: @project.id))
@merge_requests = @merge_requests.page(params[:page]).per(20) @merge_requests = @merge_requests.page(params[:page]).per(20)
@sort = params[:sort].humanize
assignee_id, milestone_id = params[:assignee_id], params[:milestone_id]
@assignee = @project.team.find(assignee_id) if assignee_id.present? && !assignee_id.to_i.zero?
@milestone = @project.milestones.find(milestone_id) if milestone_id.present? && !milestone_id.to_i.zero?
@assignees = User.where(id: @project.merge_requests.pluck(:assignee_id))
end end
def show def show
...@@ -226,6 +218,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -226,6 +218,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@allowed_to_merge = allowed_to_merge? @allowed_to_merge = allowed_to_merge?
@show_merge_controls = @merge_request.open? && @commits.any? && @allowed_to_merge @show_merge_controls = @merge_request.open? && @commits.any? && @allowed_to_merge
@source_branch = @merge_request.source_project.repository.find_branch(@merge_request.source_branch).try(:name) @source_branch = @merge_request.source_project.repository.find_branch(@merge_request.source_branch).try(:name)
if @merge_request.locked_long_ago?
@merge_request.unlock_mr
@merge_request.close
end
end end
def allowed_to_merge? def allowed_to_merge?
......
...@@ -61,10 +61,6 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -61,10 +61,6 @@ class Projects::NotesController < Projects::ApplicationController
end end
end end
def preview
render text: view_context.markdown(params[:note])
end
private private
def note def note
......
...@@ -147,6 +147,10 @@ class ProjectsController < ApplicationController ...@@ -147,6 +147,10 @@ class ProjectsController < ApplicationController
render json: { star_count: @project.star_count } render json: { star_count: @project.star_count }
end end
def markdown_preview
render text: view_context.markdown(params[:md_text])
end
private private
def upload_path def upload_path
......
...@@ -33,6 +33,7 @@ class IssuableFinder ...@@ -33,6 +33,7 @@ class IssuableFinder
items = by_search(items) items = by_search(items)
items = by_milestone(items) items = by_milestone(items)
items = by_assignee(items) items = by_assignee(items)
items = by_author(items)
items = by_label(items) items = by_label(items)
items = sort(items) items = sort(items)
end end
...@@ -125,6 +126,14 @@ class IssuableFinder ...@@ -125,6 +126,14 @@ class IssuableFinder
items items
end end
def by_author(items)
if params[:author_id].present?
items = items.where(author_id: (params[:author_id] == '0' ? nil : params[:author_id]))
end
items
end
def by_label(items) def by_label(items)
if params[:label_name].present? if params[:label_name].present?
label_names = params[:label_name].split(",") label_names = params[:label_name].split(",")
......
...@@ -73,6 +73,16 @@ class MergeRequest < ActiveRecord::Base ...@@ -73,6 +73,16 @@ class MergeRequest < ActiveRecord::Base
transition locked: :reopened transition locked: :reopened
end end
after_transition any => :locked do |merge_request, transition|
merge_request.locked_at = Time.now
merge_request.save
end
after_transition :locked => (any - :locked) do |merge_request, transition|
merge_request.locked_at = nil
merge_request.save
end
state :opened state :opened
state :reopened state :reopened
state :closed state :closed
...@@ -339,4 +349,8 @@ class MergeRequest < ActiveRecord::Base ...@@ -339,4 +349,8 @@ class MergeRequest < ActiveRecord::Base
source_project.repository.branch_names source_project.repository.branch_names
end end
end end
def locked_long_ago?
locked_at && locked_at < (Time.now - 1.day)
end
end end
...@@ -405,14 +405,8 @@ class Project < ActiveRecord::Base ...@@ -405,14 +405,8 @@ class Project < ActiveRecord::Base
end end
def execute_services(data) def execute_services(data)
services.each do |service| services.select(&:active).each do |service|
service.async_execute(data)
# Call service hook only if it is active
begin
service.execute(data) if service.active
rescue => e
logger.error(e)
end
end end
end end
......
...@@ -82,4 +82,8 @@ class Service < ActiveRecord::Base ...@@ -82,4 +82,8 @@ class Service < ActiveRecord::Base
} }
end end
end end
def async_execute(data)
Sidekiq::Client.enqueue(ProjectServiceWorker, id, data)
end
end end
...@@ -26,10 +26,6 @@ class AttachmentUploader < CarrierWave::Uploader::Base ...@@ -26,10 +26,6 @@ class AttachmentUploader < CarrierWave::Uploader::Base
Gitlab.config.gitlab.relative_url_root + "/files/#{model.class.to_s.underscore}/#{model.id}/#{file.filename}" Gitlab.config.gitlab.relative_url_root + "/files/#{model.class.to_s.underscore}/#{model.id}/#{file.filename}"
end end
def url
Gitlab.config.gitlab.relative_url_root + super unless super.nil?
end
def file_storage? def file_storage?
self.class.storage == CarrierWave::Storage::File self.class.storage == CarrierWave::Storage::File
end end
......
.issues-filters
.dropdown.inline
%a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
%i.fa.fa-user
%span.light assignee:
- if @assignee.present?
%strong= @assignee.name
- elsif params[:assignee_id] == "0"
Unassigned
- else
Any
%b.caret
%ul.dropdown-menu
%li
= link_to project_filter_path(assignee_id: nil) do
Any
= link_to project_filter_path(assignee_id: 0) do
Unassigned
- @assignees.sort_by(&:name).each do |user|
%li
= link_to project_filter_path(assignee_id: user.id) do
= image_tag avatar_icon(user.email), class: "avatar s16", alt: ''
= user.name
.dropdown.inline.prepend-left-10
%a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
%i.fa.fa-user
%span.light author:
- if @author.present?
%strong= @author.name
- elsif params[:author_id] == "0"
Unassigned
- else
Any
%b.caret
%ul.dropdown-menu
%li
= link_to project_filter_path(author_id: nil) do
Any
= link_to project_filter_path(author_id: 0) do
Unassigned
- @authors.sort_by(&:name).each do |user|
%li
= link_to project_filter_path(author_id: user.id) do
= image_tag avatar_icon(user.email), class: "avatar s16", alt: ''
= user.name
.dropdown.inline.prepend-left-10
%a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
%i.fa.fa-clock-o
%span.light milestone:
- if @milestone.present?
%strong= @milestone.title
- elsif params[:milestone_id] == "0"
None (backlog)
- else
Any
%b.caret
%ul.dropdown-menu
%li
= link_to project_filter_path(milestone_id: nil) do
Any
= link_to project_filter_path(milestone_id: 0) do
None (backlog)
- project_active_milestones.each do |milestone|
%li
= link_to project_filter_path(milestone_id: milestone.id) do
%strong= milestone.title
%small.light= milestone.expires_at
.pull-right
= render 'shared/sort_dropdown'
...@@ -14,6 +14,8 @@ ...@@ -14,6 +14,8 @@
.form-group.issuable-description .form-group.issuable-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' do
= render 'projects/zen', f: f, attr: :description, = render 'projects/zen', f: f, attr: :description,
classes: 'description form-control' classes: 'description form-control'
.col-sm-12.hint .col-sm-12.hint
...@@ -23,6 +25,7 @@ ...@@ -23,6 +25,7 @@
.pull-right .pull-right
Attach images (JPG, PNG, GIF) by dragging &amp; dropping Attach images (JPG, PNG, GIF) by dragging &amp; dropping
or #{link_to 'selecting them', '#', class: 'markdown-selector' }. or #{link_to 'selecting them', '#', class: 'markdown-selector' }.
.clearfix .clearfix
.error-alert .error-alert
%hr %hr
......
%ul.nav.nav-tabs
%li.active
= link_to '#md-write-holder', class: 'js-md-write-button' do
Write
%li
= link_to '#md-preview-holder', class: 'js-md-preview-button',
data: { url: markdown_preview_project_path(@project) } do
Preview
%div
.md-write-holder
= yield
.md-preview-holder.hide
.js-md-preview
.append-bottom-10 .append-bottom-10
.check-all-holder .check-all-holder
= check_box_tag "check_all_issues", nil, false, class: "check_all_issues left" = check_box_tag "check_all_issues", nil, false, class: "check_all_issues left"
.issues-filters = render 'projects/issuable_filter'
.dropdown.inline
%a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
%i.fa.fa-user
%span.light assignee:
- if @assignee.present?
%strong= @assignee.name
- elsif params[:assignee_id] == "0"
Unassigned
- else
Any
%b.caret
%ul.dropdown-menu
%li
= link_to project_filter_path(assignee_id: nil) do
Any
= link_to project_filter_path(assignee_id: 0) do
Unassigned
- @assignees.sort_by(&:name).each do |user|
%li
= link_to project_filter_path(assignee_id: user.id) do
= image_tag avatar_icon(user.email), class: "avatar s16", alt: ''
= user.name
.dropdown.inline.prepend-left-10
%a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
%i.fa.fa-clock-o
%span.light milestone:
- if @milestone.present?
%strong= @milestone.title
- elsif params[:milestone_id] == "0"
None (backlog)
- else
Any
%b.caret
%ul.dropdown-menu
%li
= link_to project_filter_path(milestone_id: nil) do
Any
= link_to project_filter_path(milestone_id: 0) do
None (backlog)
- project_active_milestones.each do |milestone|
%li
= link_to project_filter_path(milestone_id: milestone.id) do
%strong= milestone.title
%small.light= milestone.expires_at
.pull-right
= render 'shared/sort_dropdown'
.clearfix .clearfix
.issues_bulk_update.hide .issues_bulk_update.hide
......
...@@ -21,6 +21,7 @@ ...@@ -21,6 +21,7 @@
.form-group .form-group
.light .light
= f.label :description, "Description" = f.label :description, "Description"
= render layout: 'projects/md_preview' do
= render 'projects/zen', f: f, attr: :description, = render 'projects/zen', f: f, attr: :description,
classes: 'description form-control' classes: 'description form-control'
.clearfix.hint .clearfix.hint
......
...@@ -5,56 +5,8 @@ ...@@ -5,56 +5,8 @@
= render 'shared/project_filter', project_entities_path: project_merge_requests_path(@project), = render 'shared/project_filter', project_entities_path: project_merge_requests_path(@project),
labels: true, redirect: 'merge_requests', entity: 'merge_request' labels: true, redirect: 'merge_requests', entity: 'merge_request'
.col-md-9 .col-md-9
.mr-filters.append-bottom-10 .append-bottom-10
.dropdown.inline = render 'projects/issuable_filter'
%a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
%i.fa.fa-user
%span.light assignee:
- if @assignee.present?
%strong= @assignee.name
- elsif params[:assignee_id] == "0"
Unassigned
- else
Any
%b.caret
%ul.dropdown-menu
%li
= link_to project_filter_path(assignee_id: nil) do
Any
= link_to project_filter_path(assignee_id: 0) do
Unassigned
- @assignees.sort_by(&:name).each do |user|
%li
= link_to project_filter_path(assignee_id: user.id) do
= image_tag avatar_icon(user.email), class: "avatar s16", alt: ''
= user.name
.dropdown.inline.prepend-left-10
%a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
%i.fa.fa-clock-o
%span.light milestone:
- if @milestone.present?
%strong= @milestone.title
- elsif params[:milestone_id] == "0"
None (backlog)
- else
Any
%b.caret
%ul.dropdown-menu
%li
= link_to project_filter_path(milestone_id: nil) do
Any
= link_to project_filter_path(milestone_id: 0) do
None (backlog)
- project_active_milestones.each do |milestone|
%li
= link_to project_filter_path(milestone_id: milestone.id) do
%strong= milestone.title
%small.light= milestone.expires_at
.pull-right
= render 'shared/sort_dropdown'
.panel.panel-default .panel.panel-default
%ul.well-list.mr-list %ul.well-list.mr-list
= render @merge_requests = render @merge_requests
......
...@@ -11,13 +11,17 @@ ...@@ -11,13 +11,17 @@
- if @merge_request.closed? - if @merge_request.closed?
%h4 %h4
Closed by #{link_to_member(@project, @merge_request.closed_event.author, avatar: false)} Closed
- if @merge_request.closed_event
by #{link_to_member(@project, @merge_request.closed_event.author, avatar: false)}
#{time_ago_with_tooltip(@merge_request.closed_event.created_at)} #{time_ago_with_tooltip(@merge_request.closed_event.created_at)}
%p Changes were not merged into target branch %p Changes were not merged into target branch
- if @merge_request.merged? - if @merge_request.merged?
%h4 %h4
Merged by #{link_to_member(@project, @merge_request.merge_event.author, avatar: false)} Merged
- if @merge_request.merge_event
by #{link_to_member(@project, @merge_request.merge_event.author, avatar: false)}
#{time_ago_with_tooltip(@merge_request.merge_event.created_at)} #{time_ago_with_tooltip(@merge_request.merge_event.created_at)}
= render "projects/merge_requests/show/remove_source_branch" = render "projects/merge_requests/show/remove_source_branch"
...@@ -44,4 +48,3 @@ ...@@ -44,4 +48,3 @@
Accepting this merge request will close #{@closes_issues.size == 1 ? 'issue' : 'issues'} Accepting this merge request will close #{@closes_issues.size == 1 ? 'issue' : 'issues'}
= succeed '.' do = succeed '.' do
!= gfm(issues_sentence(@closes_issues)) != gfm(issues_sentence(@closes_issues))
...@@ -18,9 +18,10 @@ ...@@ -18,9 +18,10 @@
.col-sm-10 .col-sm-10
= f.text_field :title, maxlength: 255, class: "form-control" = f.text_field :title, maxlength: 255, class: "form-control"
%p.hint Required %p.hint Required
.form-group .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' do
= render 'projects/zen', f: f, attr: :description, classes: 'description form-control' = render 'projects/zen', f: f, attr: :description, classes: 'description form-control'
.hint .hint
.pull-left Milestones are parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"), target: '_blank'}. .pull-left Milestones are parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"), target: '_blank'}.
......
...@@ -5,23 +5,13 @@ ...@@ -5,23 +5,13 @@
= f.hidden_field :noteable_id = f.hidden_field :noteable_id
= f.hidden_field :noteable_type = f.hidden_field :noteable_type
%ul.nav.nav-tabs = render layout: 'projects/md_preview' do
%li.active
= link_to '#note-write-holder', class: 'js-note-write-button' do
Write
%li
= link_to '#note-preview-holder', class: 'js-note-preview-button', data: { url: preview_project_notes_path(@project) } do
Preview
%div
.note-write-holder
= render 'projects/zen', f: f, attr: :note, = render 'projects/zen', f: f, attr: :note,
classes: 'note_text js-note-text' classes: 'note_text js-note-text'
.light.clearfix .light.clearfix
.pull-left Comments are parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"),{ target: '_blank', tabindex: -1 }} .pull-left Comments are parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"),{ target: '_blank', tabindex: -1 }}
.pull-right Attach images (JPG, PNG, GIF) by dragging &amp; dropping or #{link_to "selecting them", '#', class: 'markdown-selector', tabindex: -1 }. .pull-right Attach images (JPG, PNG, GIF) by dragging &amp; dropping or #{link_to "selecting them", '#', class: 'markdown-selector', tabindex: -1 }.
.note-preview-holder.hide
.js-note-preview
.note-form-actions .note-form-actions
.buttons .buttons
......
...@@ -40,7 +40,8 @@ ...@@ -40,7 +40,8 @@
.note-edit-form .note-edit-form
= form_for note, url: project_note_path(@project, note), method: :put, remote: true, authenticity_token: true do |f| = form_for note, url: project_note_path(@project, note), method: :put, remote: true, authenticity_token: true do |f|
= f.text_area :note, class: 'note_text js-note-text js-gfm-input turn-on' = render layout: 'projects/md_preview' do
= f.text_area :note, class: 'note_text js-note-text markdown-area js-gfm-input turn-on'
.form-actions.clearfix .form-actions.clearfix
= f.submit 'Save changes', class: "btn btn-primary btn-save js-comment-button" = f.submit 'Save changes', class: "btn btn-primary btn-save js-comment-button"
......
class ProjectServiceWorker
include Sidekiq::Worker
sidekiq_options queue: :project_web_hook
def perform(hook_id, data)
Service.find(hook_id).execute(data)
end
end
...@@ -15,7 +15,7 @@ Sidekiq.configure_server do |config| ...@@ -15,7 +15,7 @@ Sidekiq.configure_server do |config|
config.server_middleware do |chain| config.server_middleware do |chain|
chain.add Gitlab::SidekiqMiddleware::ArgumentsLogger if ENV['SIDEKIQ_LOG_ARGUMENTS'] chain.add Gitlab::SidekiqMiddleware::ArgumentsLogger if ENV['SIDEKIQ_LOG_ARGUMENTS']
chain.add Gitlab::SidekiqMiddleware::MemoryKiller if ENV['SIDEKIQ_MAX_RSS'] chain.add Gitlab::SidekiqMiddleware::MemoryKiller if ENV['SIDEKIQ_MEMORY_KILLER_MAX_RSS']
end end
end end
......
...@@ -208,11 +208,12 @@ Gitlab::Application.routes.draw do ...@@ -208,11 +208,12 @@ Gitlab::Application.routes.draw do
post :unarchive post :unarchive
post :upload_image post :upload_image
post :toggle_star post :toggle_star
get :markdown_preview
get :autocomplete_sources get :autocomplete_sources
end end
scope module: :projects do scope module: :projects do
resources :blob, only: [:show, :destroy], constraints: { id: /.+/ } do resources :blob, only: [:show, :destroy], constraints: { id: /.+/, format: false } do
get :diff, on: :member get :diff, on: :member
end end
resources :raw, only: [:show], constraints: {id: /.+/} resources :raw, only: [:show], constraints: {id: /.+/}
...@@ -355,10 +356,6 @@ Gitlab::Application.routes.draw do ...@@ -355,10 +356,6 @@ Gitlab::Application.routes.draw do
member do member do
delete :delete_attachment delete :delete_attachment
end end
collection do
post :preview
end
end end
end end
......
...@@ -13,9 +13,9 @@ ...@@ -13,9 +13,9 @@
# #
# ENV['RAILS_RELATIVE_URL_ROOT'] = "/gitlab" # ENV['RAILS_RELATIVE_URL_ROOT'] = "/gitlab"
# Use at least one worker per core if you're on a dedicated server, # Read about unicorn workers here:
# more will usually help for _short_ waits on databases/caches. # http://doc.gitlab.com/ee/install/requirements.html#unicorn-workers
# The minimum is 2 #
worker_processes 2 worker_processes 2
# Since Unicorn is never exposed to outside clients, it does not need to # Since Unicorn is never exposed to outside clients, it does not need to
......
...@@ -8,9 +8,11 @@ class AddIdentityTable < ActiveRecord::Migration ...@@ -8,9 +8,11 @@ class AddIdentityTable < ActiveRecord::Migration
add_index :identities, :user_id add_index :identities, :user_id
User.where("provider IS NOT NULL").find_each do |user| execute <<eos
execute "INSERT INTO identities(provider, extern_uid, user_id) VALUES('#{user.provider}', '#{user.extern_uid}', '#{user.id}')" INSERT INTO identities (provider, extern_uid, user_id)
end SELECT provider, extern_uid, id FROM users
WHERE provider IS NOT NULL
eos
remove_column :users, :extern_uid remove_column :users, :extern_uid
remove_column :users, :provider remove_column :users, :provider
...@@ -20,11 +22,15 @@ class AddIdentityTable < ActiveRecord::Migration ...@@ -20,11 +22,15 @@ class AddIdentityTable < ActiveRecord::Migration
add_column :users, :extern_uid, :string add_column :users, :extern_uid, :string
add_column :users, :provider, :string add_column :users, :provider, :string
User.where("id IN(SELECT user_id FROM identities)").find_each do |user| if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
identity = user.identities.last execute <<eos
user.extern_uid = identity.extern_uid UPDATE users u
user.provider = identity.provider SET provider = i.provider, extern_uid = i.extern_uid
user.save FROM identities i
WHERE i.user_id = u.id
eos
else
execute "UPDATE users u, identities i SET u.provider = i.provider, u.extern_uid = i.extern_uid WHERE u.id = i.user_id"
end end
drop_table :identities drop_table :identities
......
class AddLockedAtToMergeRequest < ActiveRecord::Migration
def change
add_column :merge_requests, :locked_at, :datetime
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20141126120926) do ActiveRecord::Schema.define(version: 20141205134006) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -226,6 +226,7 @@ ActiveRecord::Schema.define(version: 20141126120926) do ...@@ -226,6 +226,7 @@ ActiveRecord::Schema.define(version: 20141126120926) do
t.integer "iid" t.integer "iid"
t.text "description" t.text "description"
t.integer "position", default: 0 t.integer "position", default: 0
t.datetime "locked_at"
end end
add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree
......
# Guidelines for shell commands in the GitLab codebase # Guidelines for shell commands in the GitLab codebase
This document contains guidelines for working with processes and files in the GitLab codebase.
These guidelines are meant to make your code more reliable _and_ secure.
## References ## References
- [Google Ruby Security Reviewer's Guide](https://code.google.com/p/ruby-security/wiki/Guide) - [Google Ruby Security Reviewer's Guide](https://code.google.com/p/ruby-security/wiki/Guide)
...@@ -109,3 +112,63 @@ logs = IO.popen(%W(git log), chdir: repo_dir).read ...@@ -109,3 +112,63 @@ logs = IO.popen(%W(git log), chdir: repo_dir).read
``` ```
Note that unlike `Gitlab::Popen.popen`, `IO.popen` does not capture standard error. Note that unlike `Gitlab::Popen.popen`, `IO.popen` does not capture standard error.
## Avoid user input at the start of path strings
Various methods for opening and reading files in Ruby can be used to read the
standard output of a process instead of a file. The following two commands do
roughly the same:
```
`touch /tmp/pawned-by-backticks`
File.read('|touch /tmp/pawned-by-file-read')
```
The key is to open a 'file' whose name starts with a `|`.
Affected methods include Kernel#open, File::read, File::open, IO::open and IO::read.
You can protect against this behavior of 'open' and 'read' by ensuring that an
attacker cannot control the start of the filename string you are opening. For
instance, the following is sufficient to protect against accidentally starting
a shell command with `|`:
```
# we assume repo_path is not controlled by the attacker (user)
path = File.join(repo_path, user_input)
# path cannot start with '|' now.
File.read(path)
```
## Guard against path traversal
Path traversal is a security where the program (GitLab) tries to restrict user
access to a certain directory on disk, but the user manages to open a file
outside that directory by taking advantage of the `../` path notation.
```
# Suppose the user gave us a path and they are trying to trick us
user_input = '../other-repo.git/other-file'
# We look up the repo path somewhere
repo_path = 'repositories/user-repo.git'
# The intention of the code below is to open a file under repo_path, but
# because the user used '..' she can 'break out' into
# 'repositories/other-repo.git'
full_path = File.join(repo_path, user_input)
File.open(full_path) do # Oops!
```
A good way to protect against this is to compare the full path with its
'absolute path' according to Ruby's `File.absolute_path`.
```
full_path = File.join(repo_path, user_input)
if full_path != File.absolute_path(full_path)
raise "Invalid path: #{full_path.inspect}"
end
File.open(full_path) do # Etc.
```
A check like this could have avoided CVE-2013-4583.
...@@ -38,6 +38,16 @@ We love [JRuby](http://jruby.org/) and [Rubinius](http://rubini.us/) but GitLab ...@@ -38,6 +38,16 @@ We love [JRuby](http://jruby.org/) and [Rubinius](http://rubini.us/) but GitLab
## Hardware requirements ## Hardware requirements
### Storage
The necessary hard drive space largely depends on the size of the repos you want to store in GitLab but as a *rule of thumb* you should have at least twice as much free space as all your repos combined take up. You need twice the storage because [GitLab satellites](structure.md) contain an extra copy of each repo.
If you want to be flexible about growing your hard drive space in the future consider mounting it using LVM so you can add more hard drives when you need them.
Apart from a local hard drive you can also mount a volume that supports the network file system (NFS) protocol. This volume might be located on a file server, a network attached storage (NAS) device, a storage area network (SAN) or on an Amazon Web Services (AWS) Elastic Block Store (EBS) volume.
If you have enough RAM memory and a recent CPU the speed of GitLab is mainly limited by hard drive seek times. Having a fast drive (7200 RPM and up) or a solid state drive (SSD) will improve the responsiveness of GitLab.
### CPU ### CPU
- 1 core works supports up to 100 users but the application can be a bit slower due to having all workers and background jobs running on the same core - 1 core works supports up to 100 users but the application can be a bit slower due to having all workers and background jobs running on the same core
...@@ -50,12 +60,10 @@ We love [JRuby](http://jruby.org/) and [Rubinius](http://rubini.us/) but GitLab ...@@ -50,12 +60,10 @@ We love [JRuby](http://jruby.org/) and [Rubinius](http://rubini.us/) but GitLab
### Memory ### Memory
- 512MB is the absolute minimum but we do not recommend this amount of memory. You need at least 2GB of addressable memory (RAM + swap) to install and use GitLab!
You will need to configure minimum 1.5GB of swap space. With less memory GitLab will give strange errors during the reconfigure run and 500 errors during usage.
With 1.5GB of swap space you must configure only one unicorn worker.
With one unicorn worker only git over ssh access will work because the git over HTTP access requires two running workers (one worker to receive the user request and one worker for the authorization check). - 512MB RAM + 1.5GB of swap is the absolute minimum but we strongly **advise against** this amount of memory. See the unicorn worker section below for more advise.
If you use SSD storage and configure 1.5GB of swap space you can use two Unicorn workers, this will allow HTTP access but it will still be slow.
Consider installing GitLab on Ubuntu as installation on CentOS could be unsuccessful with this amount of memory.
- 1GB RAM + 1GB swap supports up to 100 users - 1GB RAM + 1GB swap supports up to 100 users
- **2GB RAM** is the **recommended** memory size and supports up to 500 users - **2GB RAM** is the **recommended** memory size and supports up to 500 users
- 4GB RAM supports up to 2,000 users - 4GB RAM supports up to 2,000 users
...@@ -66,15 +74,16 @@ Consider installing GitLab on Ubuntu as installation on CentOS could be unsucces ...@@ -66,15 +74,16 @@ Consider installing GitLab on Ubuntu as installation on CentOS could be unsucces
Notice: The 25 workers of Sidekiq will show up as separate processes in your process overview (such as top or htop) but they share the same RAM allocation since Sidekiq is a multithreaded application. Notice: The 25 workers of Sidekiq will show up as separate processes in your process overview (such as top or htop) but they share the same RAM allocation since Sidekiq is a multithreaded application.
### Storage ## Unicorn Workers
The necessary hard drive space largely depends on the size of the repos you want to store in GitLab but as a *rule of thumb* you should have at least twice as much free space as all your repos combined take up. You need twice the storage because [GitLab satellites](structure.md) contain an extra copy of each repo. It's possible to increase the amount of unicorn workers and tis will usually help for to reduce the response time of the applications.
For most instances we recommend using: CPU cores + 1 = unicorn workers.
So for a machine with 2 cores, 3 unicorn workers is ideal.
If you want to be flexible about growing your hard drive space in the future consider mounting it using LVM so you can add more hard drives when you need them. For all machines that have 1GB and up we recommend a minimum of two unicorn workers.
If you have a 512MB machine with a magnetic (non-SSD) swap drive we recommend to configure only one Unicorn worker to prevent excessive swapping.
Apart from a local hard drive you can also mount a volume that supports the network file system (NFS) protocol. This volume might be located on a file server, a network attached storage (NAS) device, a storage area network (SAN) or on an Amazon Web Services (AWS) Elastic Block Store (EBS) volume. With one Unicorn worker only git over ssh access will work because the git over HTTP access requires two running workers (one worker to receive the user request and one worker for the authorization check).
If you have a 512MB machine with a SSD drive you can use two Unicorn workers, this will allow HTTP access although it will be slow due to swapping.
If you have enough RAM memory and a recent CPU the speed of GitLab is mainly limited by hard drive seek times. Having a fast drive (7200 RPM and up) or a solid state drive (SSD) will improve the responsiveness of GitLab.
## Database ## Database
......
...@@ -13,7 +13,7 @@ To enable the Twitter OmniAuth provider you must register your application with ...@@ -13,7 +13,7 @@ To enable the Twitter OmniAuth provider you must register your application with
something else descriptive. something else descriptive.
- Description: Create a description. - Description: Create a description.
- Website: The URL to your GitLab installation. 'https://gitlab.example.com' - Website: The URL to your GitLab installation. 'https://gitlab.example.com'
- Callback URL: 'https://gitlab.example.com/users/auth/github/callback' - Callback URL: 'https://gitlab.example.com/users/auth/twitter/callback'
- Agree to the "Rules of the Road." - Agree to the "Rules of the Road."
![Twitter App Details](twitter_app_details.png) ![Twitter App Details](twitter_app_details.png)
......
...@@ -208,3 +208,26 @@ Add the following lines at the bottom: ...@@ -208,3 +208,26 @@ Add the following lines at the bottom:
The `CRON=1` environment setting tells the backup script to suppress all progress output if there are no errors. The `CRON=1` environment setting tells the backup script to suppress all progress output if there are no errors.
This is recommended to reduce cron spam. This is recommended to reduce cron spam.
## Alternative backup strategies
If your GitLab server contains a lot of Git repository data you may find the GitLab backup script to be too slow.
In this case you can consider using filesystem snapshots as part of your backup strategy.
Example: Amazone EBS
> A GitLab server using omnibus-gitlab hosted on Amazon AWS.
> An EBS drive containing an ext4 filesystem is mounted at `/var/opt/gitlab`.
> In this case you could make an application backup by taking an EBS snapshot.
> The backup includes all repositories, uploads and Postgres data.
Example: LVM snapshots + Rsync
> A GitLab server using omnibus-gitlab, with an LVM logical volume mounted at `/var/opt/gitlab`.
> Replicating the `/var/opt/gitlab` directory usign Rsync would not be reliable because too many files would change while Rsync is running.
> Instead of rsync-ing `/var/opt/gitlab`, we create a temporary LVM snapshot, which we mount as a read-only filesystem at `/mnt/gitlab_backup`.
> Now we can have a longer running Rsync job which will create a consistent replica on the remote server.
> The replica includes all repositories, uploads and Postgres data.
If you are running GitLab on a virtualized server you can possibly also create VM snapshots of the entire GitLab server.
It is not uncommon however for a VM snapshot to require you to power down the server, so this approach is probably of limited practical use.
...@@ -18,6 +18,7 @@ Otherwise include it in the monthly release and note there was a regression fix ...@@ -18,6 +18,7 @@ Otherwise include it in the monthly release and note there was a regression fix
1. Name the issue "Release X.X.X CE and X.X.X EE", this will make searching easier 1. Name the issue "Release X.X.X CE and X.X.X EE", this will make searching easier
1. Fix the issue on a feature branch, do this on the private GitLab development server 1. Fix the issue on a feature branch, do this on the private GitLab development server
1. If it is a security issue, then assign it to the release manager and apply a 'security' label 1. If it is a security issue, then assign it to the release manager and apply a 'security' label
1. Build the package for GitLab.com and do a deploy
1. Consider creating and testing workarounds 1. Consider creating and testing workarounds
1. After the branch is merged into master, cherry pick the commit(s) into the current stable branch 1. After the branch is merged into master, cherry pick the commit(s) into the current stable branch
1. Make sure that the build has passed and all tests are passing 1. Make sure that the build has passed and all tests are passing
......
...@@ -17,6 +17,7 @@ Please report suspected security vulnerabilities in private to <support@gitlab.c ...@@ -17,6 +17,7 @@ Please report suspected security vulnerabilities in private to <support@gitlab.c
1. Inform the release manager that there needs to be a security release 1. Inform the release manager that there needs to be a security release
1. Do the steps from [patch release document](doc/release/patch.md), starting with "Create an issue on private GitLab development server" 1. Do the steps from [patch release document](doc/release/patch.md), starting with "Create an issue on private GitLab development server"
1. The MR with the security fix should get a 'security' label and be assigned to the release manager 1. The MR with the security fix should get a 'security' label and be assigned to the release manager
1. Build the package for GitLab.com and do a deploy
1. Create feature branches for the blog post on GitLab.com and link them from the code branch 1. Create feature branches for the blog post on GitLab.com and link them from the code branch
1. Merge and publish the blog posts 1. Merge and publish the blog posts
1. Send tweets about the release from `@gitlabhq` 1. Send tweets about the release from `@gitlabhq`
......
...@@ -16,12 +16,12 @@ Feature: Project Commits Comments ...@@ -16,12 +16,12 @@ Feature: Project Commits Comments
@javascript @javascript
Scenario: I can't preview without text Scenario: I can't preview without text
Given I haven't written any comment text Given I haven't written any comment text
Then I should not see the comment preview button Then The comment preview tab should say there is nothing to do
@javascript @javascript
Scenario: I can preview with text Scenario: I can preview with text
Given I write a comment like "Nice" Given I write a comment like ":+1: Nice"
Then I should see the comment preview button Then The comment preview tab should be display rendered Markdown
@javascript @javascript
Scenario: I preview a comment Scenario: I preview a comment
...@@ -32,7 +32,7 @@ Feature: Project Commits Comments ...@@ -32,7 +32,7 @@ Feature: Project Commits Comments
@javascript @javascript
Scenario: I can edit after preview Scenario: I can edit after preview
Given I preview a comment text like "Bug fixed :smile:" Given I preview a comment text like "Bug fixed :smile:"
Then I should see the comment edit button Then I should see the comment write tab
@javascript @javascript
Scenario: I have a reset form after posting from preview Scenario: I have a reset form after posting from preview
......
...@@ -58,13 +58,13 @@ Feature: Project Commits Diff Comments ...@@ -58,13 +58,13 @@ Feature: Project Commits Diff Comments
Scenario: I can't preview without text Scenario: I can't preview without text
Given I open a diff comment form Given I open a diff comment form
And I haven't written any diff comment text And I haven't written any diff comment text
Then I should not see the diff comment preview button Then The diff comment preview tab should say there is nothing to do
@javascript @javascript
Scenario: I can preview with text Scenario: I can preview with text
Given I open a diff comment form Given I open a diff comment form
And I write a diff comment like ":-1: I don't like this" And I write a diff comment like ":-1: I don't like this"
Then I should see the diff comment preview button Then The diff comment preview tab should display rendered Markdown
@javascript @javascript
Scenario: I preview a diff comment Scenario: I preview a diff comment
...@@ -75,7 +75,7 @@ Feature: Project Commits Diff Comments ...@@ -75,7 +75,7 @@ Feature: Project Commits Diff Comments
@javascript @javascript
Scenario: I can edit after preview Scenario: I can edit after preview
Given I preview a diff comment text like "Should fix it :smile:" Given I preview a diff comment text like "Should fix it :smile:"
Then I should see the diff comment edit button Then I should see the diff comment write tab
@javascript @javascript
Scenario: The form gets removed after posting Scenario: The form gets removed after posting
......
...@@ -159,3 +159,37 @@ Feature: Project Issues ...@@ -159,3 +159,37 @@ Feature: Project Issues
Given project "Shop" has "Tasks-closed" closed issue with task markdown Given project "Shop" has "Tasks-closed" closed issue with task markdown
When I visit issue page "Tasks-closed" When I visit issue page "Tasks-closed"
Then Task checkboxes should be disabled Then Task checkboxes should be disabled
# Issue description preview
@javascript
Scenario: I can't preview without text
Given I click link "New Issue"
And I haven't written any description text
Then The Markdown preview tab should say there is nothing to do
@javascript
Scenario: I can preview with text
Given I click link "New Issue"
And I write a description like ":+1: Nice"
Then The Markdown preview tab should display rendered Markdown
@javascript
Scenario: I preview an issue description
Given I click link "New Issue"
And I preview a description text like "Bug fixed :smile:"
Then I should see the Markdown preview
And I should not see the Markdown text field
@javascript
Scenario: I can edit after preview
Given I click link "New Issue"
And I preview a description text like "Bug fixed :smile:"
Then I should see the Markdown write tab
@javascript
Scenario: I can preview when editing an existing issue
Given I click link "Release 0.4"
And I click link "Edit" for the issue
And I preview a description text like "Bug fixed :smile:"
Then I should see the Markdown write tab
...@@ -192,3 +192,34 @@ Feature: Project Merge Requests ...@@ -192,3 +192,34 @@ Feature: Project Merge Requests
And I visit merge request page "MR-task-open" And I visit merge request page "MR-task-open"
And I click link "Close" And I click link "Close"
Then Task checkboxes should be disabled Then Task checkboxes should be disabled
# Description preview
@javascript
Scenario: I can't preview without text
Given I visit merge request page "Bug NS-04"
And I click link "Edit" for the merge request
And I haven't written any description text
Then The Markdown preview tab should say there is nothing to do
@javascript
Scenario: I can preview with text
Given I visit merge request page "Bug NS-04"
And I click link "Edit" for the merge request
And I write a description like ":+1: Nice"
Then The Markdown preview tab should display rendered Markdown
@javascript
Scenario: I preview a merge request description
Given I visit merge request page "Bug NS-04"
And I click link "Edit" for the merge request
And I preview a description text like "Bug fixed :smile:"
Then I should see the Markdown preview
And I should not see the Markdown text field
@javascript
Scenario: I can edit after preview
Given I visit merge request page "Bug NS-04"
And I click link "Edit" for the merge request
And I preview a description text like "Bug fixed :smile:"
Then I should see the Markdown write tab
class Spinach::Features::ProjectIssues < Spinach::FeatureSteps class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
include SharedAuthentication include SharedAuthentication
include SharedIssuable
include SharedProject include SharedProject
include SharedNote include SharedNote
include SharedPaths include SharedPaths
......
class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
include SharedAuthentication include SharedAuthentication
include SharedIssuable
include SharedProject include SharedProject
include SharedNote include SharedNote
include SharedPaths include SharedPaths
......
...@@ -32,7 +32,7 @@ module SharedDiffNote ...@@ -32,7 +32,7 @@ module SharedDiffNote
click_diff_line(sample_commit.line_code) click_diff_line(sample_commit.line_code)
within("#{diff_file_selector} form[rel$='#{sample_commit.line_code}']") do within("#{diff_file_selector} form[rel$='#{sample_commit.line_code}']") do
fill_in "note[note]", with: "Should fix it :smile:" fill_in "note[note]", with: "Should fix it :smile:"
find(".js-note-preview-button").trigger("click") find('.js-md-preview-button').click
end end
end end
...@@ -41,7 +41,7 @@ module SharedDiffNote ...@@ -41,7 +41,7 @@ module SharedDiffNote
within("#{diff_file_selector} form[rel$='#{sample_commit.del_line_code}']") do within("#{diff_file_selector} form[rel$='#{sample_commit.del_line_code}']") do
fill_in "note[note]", with: "DRY this up" fill_in "note[note]", with: "DRY this up"
find(".js-note-preview-button").trigger("click") find('.js-md-preview-button').click
end end
end end
...@@ -71,9 +71,10 @@ module SharedDiffNote ...@@ -71,9 +71,10 @@ module SharedDiffNote
end end
end end
step 'I should not see the diff comment preview button' do step 'The diff comment preview tab should say there is nothing to do' do
within(diff_file_selector) do within(diff_file_selector) do
page.should have_css(".js-note-preview-button", visible: false) find('.js-md-preview-button').click
expect(find('.js-md-preview')).to have_content('Nothing to preview.')
end end
end end
...@@ -131,27 +132,28 @@ module SharedDiffNote ...@@ -131,27 +132,28 @@ module SharedDiffNote
step 'I should see the diff comment preview' do step 'I should see the diff comment preview' do
within("#{diff_file_selector} form") do within("#{diff_file_selector} form") do
page.should have_css(".js-note-preview", visible: false) expect(page).to have_css('.js-md-preview', visible: true)
end end
end end
step 'I should see the diff comment edit button' do step 'I should see the diff comment write tab' do
within(diff_file_selector) do within(diff_file_selector) do
page.should have_css(".js-note-write-button", visible: true) expect(page).to have_css('.js-md-write-button', visible: true)
end end
end end
step 'I should see the diff comment preview button' do step 'The diff comment preview tab should display rendered Markdown' do
within(diff_file_selector) do within(diff_file_selector) do
page.should have_css(".js-note-preview-button", visible: true) find('.js-md-preview-button').click
expect(find('.js-md-preview')).to have_css('img.emoji', visible: true)
end end
end end
step 'I should see two separate previews' do step 'I should see two separate previews' do
within(diff_file_selector) do within(diff_file_selector) do
page.should have_css(".js-note-preview", visible: true, count: 2) expect(page).to have_css('.js-md-preview', visible: true, count: 2)
page.should have_content("Should fix it") expect(page).to have_content('Should fix it')
page.should have_content("DRY this up") expect(page).to have_content('DRY this up')
end end
end end
......
module SharedIssuable
include Spinach::DSL
def edit_issuable
find('.issue-btn-group').click_link 'Edit'
end
step 'I click link "Edit" for the merge request' do
edit_issuable
end
step 'I click link "Edit" for the issue' do
edit_issuable
end
end
...@@ -54,4 +54,49 @@ EOT ...@@ -54,4 +54,49 @@ EOT
'div.description li.task-list-item input[type="checkbox"]:disabled' 'div.description li.task-list-item input[type="checkbox"]:disabled'
) )
end end
step 'I should not see the Markdown preview' do
expect(find('.gfm-form .js-md-preview')).not_to be_visible
end
step 'The Markdown preview tab should say there is nothing to do' do
within('.gfm-form') do
find('.js-md-preview-button').click
expect(find('.js-md-preview')).to have_content('Nothing to preview.')
end
end
step 'I should not see the Markdown text field' do
expect(find('.gfm-form textarea')).not_to be_visible
end
step 'I should see the Markdown write tab' do
expect(find('.gfm-form')).to have_css('.js-md-write-button', visible: true)
end
step 'I should see the Markdown preview' do
expect(find('.gfm-form')).to have_css('.js-md-preview', visible: true)
end
step 'The Markdown preview tab should display rendered Markdown' do
within('.gfm-form') do
find('.js-md-preview-button').click
expect(find('.js-md-preview')).to have_css('img.emoji', visible: true)
end
end
step 'I write a description like ":+1: Nice"' do
find('.gfm-form').fill_in 'Description', with: ':+1: Nice'
end
step 'I preview a description text like "Bug fixed :smile:"' do
within('.gfm-form') do
fill_in 'Description', with: 'Bug fixed :smile:'
find('.js-md-preview-button').click
end
end
step 'I haven\'t written any description text' do
find('.gfm-form').fill_in 'Description', with: ''
end
end end
...@@ -23,7 +23,7 @@ module SharedNote ...@@ -23,7 +23,7 @@ module SharedNote
step 'I preview a comment text like "Bug fixed :smile:"' do step 'I preview a comment text like "Bug fixed :smile:"' do
within(".js-main-target-form") do within(".js-main-target-form") do
fill_in "note[note]", with: "Bug fixed :smile:" fill_in "note[note]", with: "Bug fixed :smile:"
find(".js-note-preview-button").trigger("click") find('.js-md-preview-button').click
end end
end end
...@@ -33,9 +33,9 @@ module SharedNote ...@@ -33,9 +33,9 @@ module SharedNote
end end
end end
step 'I write a comment like "Nice"' do step 'I write a comment like ":+1: Nice"' do
within(".js-main-target-form") do within(".js-main-target-form") do
fill_in "note[note]", with: "Nice" fill_in 'note[note]', with: ':+1: Nice'
end end
end end
...@@ -51,13 +51,14 @@ module SharedNote ...@@ -51,13 +51,14 @@ module SharedNote
step 'I should not see the comment preview' do step 'I should not see the comment preview' do
within(".js-main-target-form") do within(".js-main-target-form") do
page.should have_css(".js-note-preview", visible: false) expect(find('.js-md-preview')).not_to be_visible
end end
end end
step 'I should not see the comment preview button' do step 'The comment preview tab should say there is nothing to do' do
within(".js-main-target-form") do within(".js-main-target-form") do
page.should have_css(".js-note-preview-button", visible: false) find('.js-md-preview-button').click
expect(find('.js-md-preview')).to have_content('Nothing to preview.')
end end
end end
...@@ -79,21 +80,22 @@ module SharedNote ...@@ -79,21 +80,22 @@ module SharedNote
end end
end end
step 'I should see the comment edit button' do step 'I should see the comment write tab' do
within(".js-main-target-form") do within(".js-main-target-form") do
page.should have_css(".js-note-write-button", visible: true) expect(page).to have_css('.js-md-write-button', visible: true)
end end
end end
step 'I should see the comment preview' do step 'The comment preview tab should be display rendered Markdown' do
within(".js-main-target-form") do within(".js-main-target-form") do
page.should have_css(".js-note-preview", visible: true) find('.js-md-preview-button').click
expect(find('.js-md-preview')).to have_css('img.emoji', visible: true)
end end
end end
step 'I should see the comment preview button' do step 'I should see the comment preview' do
within(".js-main-target-form") do within(".js-main-target-form") do
page.should have_css(".js-note-preview-button", visible: true) expect(page).to have_css('.js-md-preview', visible: true)
end end
end end
......
...@@ -49,8 +49,17 @@ module Gitlab ...@@ -49,8 +49,17 @@ module Gitlab
end end
def push_access_check(user, project, changes) def push_access_check(user, project, changes)
return build_status_object(false, "You don't have access") unless user && user_allowed?(user) unless user && user_allowed?(user)
return build_status_object(true) if changes.blank? return build_status_object(false, "You don't have access")
end
if changes.blank?
return build_status_object(true)
end
unless project.repository.exists?
return build_status_object(false, "Repository does not exist")
end
changes = changes.lines if changes.kind_of?(String) changes = changes.lines if changes.kind_of?(String)
...@@ -79,7 +88,7 @@ module Gitlab ...@@ -79,7 +88,7 @@ module Gitlab
else else
:push_code_to_protected_branches :push_code_to_protected_branches
end end
elsif project.repository && project.repository.tag_names.include?(tag_name(ref)) elsif project.repository.tag_names.include?(tag_name(ref))
# Prevent any changes to existing git tag unless user has permissions # Prevent any changes to existing git tag unless user has permissions
:admin_project :admin_project
else else
......
module Gitlab module Gitlab
module SidekiqMiddleware module SidekiqMiddleware
class MemoryKiller class MemoryKiller
# Default the RSS limit to 0, meaning the MemoryKiller is disabled
MAX_RSS = (ENV['SIDEKIQ_MEMORY_KILLER_MAX_RSS'] || 0).to_s.to_i
# Give Sidekiq 15 minutes of grace time after exceeding the RSS limit
GRACE_TIME = (ENV['SIDEKIQ_MEMORY_KILLER_GRACE_TIME'] || 15 * 60).to_s.to_i
# Wait 30 seconds for running jobs to finish during graceful shutdown # Wait 30 seconds for running jobs to finish during graceful shutdown
GRACEFUL_SHUTDOWN_WAIT = 30 SHUTDOWN_WAIT = (ENV['SIDEKIQ_MEMORY_KILLER_SHUTDOWN_WAIT'] || 30).to_s.to_i
# Create a mutex used to ensure there will be only one thread waiting to
# shut Sidekiq down
MUTEX = Mutex.new
def call(worker, job, queue) def call(worker, job, queue)
yield yield
current_rss = get_rss current_rss = get_rss
return unless max_rss > 0 && current_rss > max_rss
return unless MAX_RSS > 0 && current_rss > MAX_RSS
Thread.new do
# Return if another thread is already waiting to shut Sidekiq down
return unless MUTEX.try_lock
Sidekiq.logger.warn "current RSS #{current_rss} exceeds maximum RSS "\ Sidekiq.logger.warn "current RSS #{current_rss} exceeds maximum RSS "\
"#{max_rss}" "#{MAX_RSS}"
Sidekiq.logger.warn "spawned thread that will shut down PID "\
"#{Process.pid} in #{GRACE_TIME} seconds"
sleep(GRACE_TIME)
Sidekiq.logger.warn "sending SIGUSR1 to PID #{Process.pid}" Sidekiq.logger.warn "sending SIGUSR1 to PID #{Process.pid}"
# SIGUSR1 tells Sidekiq to stop accepting new jobs
Process.kill('SIGUSR1', Process.pid) Process.kill('SIGUSR1', Process.pid)
Sidekiq.logger.warn "spawning thread that will send SIGTERM to PID "\ Sidekiq.logger.warn "waiting #{SHUTDOWN_WAIT} seconds before sending "\
"#{Process.pid} in #{graceful_shutdown_wait} seconds" "SIGTERM to PID #{Process.pid}"
# Send the final shutdown signal to Sidekiq from a separate thread so sleep(SHUTDOWN_WAIT)
# that the current job can finish
Thread.new do Sidekiq.logger.warn "sending SIGTERM to PID #{Process.pid}"
sleep(graceful_shutdown_wait)
Process.kill('SIGTERM', Process.pid) Process.kill('SIGTERM', Process.pid)
end end
end end
...@@ -33,16 +48,6 @@ module Gitlab ...@@ -33,16 +48,6 @@ module Gitlab
output.to_i output.to_i
end end
def max_rss
@max_rss ||= ENV['SIDEKIQ_MAX_RSS'].to_s.to_i
end
def graceful_shutdown_wait
@graceful_shutdown_wait ||= (
ENV['SIDEKIQ_GRACEFUL_SHUTDOWN_WAIT'] || GRACEFUL_SHUTDOWN_WAIT
).to_i
end
end end
end end
end end
...@@ -19,8 +19,9 @@ describe 'Comments' do ...@@ -19,8 +19,9 @@ describe 'Comments' do
it 'should be valid' do it 'should be valid' do
should have_css(".js-main-target-form", visible: true, count: 1) should have_css(".js-main-target-form", visible: true, count: 1)
find(".js-main-target-form input[type=submit]").value.should == "Add Comment" find(".js-main-target-form input[type=submit]").value.should == "Add Comment"
within(".js-main-target-form") { should_not have_link("Cancel") } within('.js-main-target-form') do
within(".js-main-target-form") { should have_css(".js-note-preview-button", visible: false) } expect(page).not_to have_link('Cancel')
end
end end
describe "with text" do describe "with text" do
...@@ -31,8 +32,10 @@ describe 'Comments' do ...@@ -31,8 +32,10 @@ describe 'Comments' do
end end
it 'should have enable submit button and preview button' do it 'should have enable submit button and preview button' do
within(".js-main-target-form") { should_not have_css(".js-comment-button[disabled]") } within('.js-main-target-form') do
within(".js-main-target-form") { should have_css(".js-note-preview-button", visible: true) } expect(page).not_to have_css('.js-comment-button[disabled]')
expect(page).to have_css('.js-md-preview-button', visible: true)
end
end end
end end
end end
...@@ -41,15 +44,17 @@ describe 'Comments' do ...@@ -41,15 +44,17 @@ describe 'Comments' do
before do before do
within(".js-main-target-form") do within(".js-main-target-form") do
fill_in "note[note]", with: "This is awsome!" fill_in "note[note]", with: "This is awsome!"
find(".js-note-preview-button").trigger("click") find('.js-md-preview-button').click
click_button "Add Comment" click_button "Add Comment"
end end
end end
it 'should be added and form reset' do it 'should be added and form reset' do
should have_content("This is awsome!") should have_content("This is awsome!")
within(".js-main-target-form") { should have_no_field("note[note]", with: "This is awesome!") } within('.js-main-target-form') do
within(".js-main-target-form") { should have_css(".js-note-preview", visible: false) } expect(page).to have_no_field('note[note]', with: 'This is awesome!')
expect(page).to have_css('.js-md-preview', visible: :hidden)
end
within(".js-main-target-form") { should have_css(".js-note-text", visible: true) } within(".js-main-target-form") { should have_css(".js-note-text", visible: true) }
end end
end end
...@@ -172,11 +177,11 @@ describe 'Comments' do ...@@ -172,11 +177,11 @@ describe 'Comments' do
# add two separate texts and trigger previews on both # add two separate texts and trigger previews on both
within("tr[id='#{line_code}'] + .js-temp-notes-holder") do within("tr[id='#{line_code}'] + .js-temp-notes-holder") do
fill_in "note[note]", with: "One comment on line 7" fill_in "note[note]", with: "One comment on line 7"
find(".js-note-preview-button").trigger("click") find('.js-md-preview-button').click
end end
within("tr[id='#{line_code_2}'] + .js-temp-notes-holder") do within("tr[id='#{line_code_2}'] + .js-temp-notes-holder") do
fill_in "note[note]", with: "Another comment on line 10" fill_in "note[note]", with: "Another comment on line 10"
find(".js-note-preview-button").trigger("click") find('.js-md-preview-button').click
end end
end end
end end
......
...@@ -5,9 +5,10 @@ describe IssuesFinder do ...@@ -5,9 +5,10 @@ describe IssuesFinder do
let(:user2) { create :user } let(:user2) { create :user }
let(:project1) { create(:project) } let(:project1) { create(:project) }
let(:project2) { create(:project) } let(:project2) { create(:project) }
let(:issue1) { create(:issue, assignee: user, project: project1) } let(:milestone) { create(:milestone, project: project1) }
let(:issue2) { create(:issue, assignee: user, project: project2) } let(:issue1) { create(:issue, author: user, assignee: user, project: project1, milestone: milestone) }
let(:issue3) { create(:issue, assignee: user2, project: project2) } let(:issue2) { create(:issue, author: user, assignee: user, project: project2) }
let(:issue3) { create(:issue, author: user2, assignee: user2, project: project2) }
before do before do
project1.team << [user, :master] project1.team << [user, :master]
...@@ -22,22 +23,29 @@ describe IssuesFinder do ...@@ -22,22 +23,29 @@ describe IssuesFinder do
issue3 issue3
end end
context 'scope: all' do
it 'should filter by all' do it 'should filter by all' do
params = { scope: "all", state: 'opened' } params = { scope: "all", state: 'opened' }
issues = IssuesFinder.new.execute(user, params) issues = IssuesFinder.new.execute(user, params)
issues.size.should == 3 issues.size.should == 3
end end
it 'should filter by assignee' do it 'should filter by assignee id' do
params = { scope: "assigned-to-me", state: 'opened' } params = { scope: "all", assignee_id: user.id, state: 'opened' }
issues = IssuesFinder.new.execute(user, params) issues = IssuesFinder.new.execute(user, params)
issues.size.should == 2 issues.size.should == 2
end end
it 'should filter by project' do it 'should filter by author id' do
params = { scope: "assigned-to-me", state: 'opened', project_id: project1.id } params = { scope: "all", author_id: user2.id, state: 'opened' }
issues = IssuesFinder.new.execute(user, params) issues = IssuesFinder.new.execute(user, params)
issues.size.should == 1 issues.should == [issue3]
end
it 'should filter by milestone id' do
params = { scope: "all", milestone_id: milestone.id, state: 'opened' }
issues = IssuesFinder.new.execute(user, params)
issues.should == [issue1]
end end
it 'should be empty for unauthorized user' do it 'should be empty for unauthorized user' do
...@@ -55,4 +63,19 @@ describe IssuesFinder do ...@@ -55,4 +63,19 @@ describe IssuesFinder do
issues.should include(issue3) issues.should include(issue3)
end end
end end
context 'personal scope' do
it 'should filter by assignee' do
params = { scope: "assigned-to-me", state: 'opened' }
issues = IssuesFinder.new.execute(user, params)
issues.size.should == 2
end
it 'should filter by project' do
params = { scope: "assigned-to-me", state: 'opened', project_id: project1.id }
issues = IssuesFinder.new.execute(user, params)
issues.size.should == 1
end
end
end
end end
...@@ -66,6 +66,16 @@ describe ApplicationHelper do ...@@ -66,6 +66,16 @@ describe ApplicationHelper do
avatar_icon(user.email).to_s.should match("/uploads/user/avatar/#{ user.id }/gitlab_logo.png") avatar_icon(user.email).to_s.should match("/uploads/user/avatar/#{ user.id }/gitlab_logo.png")
end end
it "should return an url for the avatar with relative url" do
Gitlab.config.gitlab.stub(relative_url_root: "/gitlab")
Gitlab.config.gitlab.stub(url: Settings.send(:build_gitlab_url))
user = create(:user)
user.avatar = File.open(avatar_file_path)
user.save!
avatar_icon(user.email).to_s.should match("/gitlab//uploads/user/avatar/#{ user.id }/gitlab_logo.png")
end
it "should call gravatar_icon when no avatar is present" do it "should call gravatar_icon when no avatar is present" do
user = create(:user, email: 'test@example.com') user = create(:user, email: 'test@example.com')
user.save! user.save!
......
...@@ -60,6 +60,7 @@ end ...@@ -60,6 +60,7 @@ end
# project GET /:id(.:format) projects#show # project GET /:id(.:format) projects#show
# PUT /:id(.:format) projects#update # PUT /:id(.:format) projects#update
# DELETE /:id(.:format) projects#destroy # DELETE /:id(.:format) projects#destroy
# markdown_preview_project GET /:id/markdown_preview(.:format) projects#markdown_preview
describe ProjectsController, "routing" do describe ProjectsController, "routing" do
it "to #create" do it "to #create" do
post("/projects").should route_to('projects#create') post("/projects").should route_to('projects#create')
...@@ -88,6 +89,12 @@ describe ProjectsController, "routing" do ...@@ -88,6 +89,12 @@ describe ProjectsController, "routing" do
it "to #destroy" do it "to #destroy" do
delete("/gitlab/gitlabhq").should route_to('projects#destroy', id: 'gitlab/gitlabhq') delete("/gitlab/gitlabhq").should route_to('projects#destroy', id: 'gitlab/gitlabhq')
end end
it 'to #markdown_preview' do
get('/gitlab/gitlabhq/markdown_preview').should(
route_to('projects#markdown_preview', id: 'gitlab/gitlabhq')
)
end
end end
# pages_project_wikis GET /:project_id/wikis/pages(.:format) projects/wikis#pages # pages_project_wikis GET /:project_id/wikis/pages(.:format) projects/wikis#pages
...@@ -387,15 +394,10 @@ describe Projects::IssuesController, "routing" do ...@@ -387,15 +394,10 @@ describe Projects::IssuesController, "routing" do
end end
end end
# preview_project_notes POST /:project_id/notes/preview(.:format) notes#preview
# project_notes GET /:project_id/notes(.:format) notes#index # project_notes GET /:project_id/notes(.:format) notes#index
# POST /:project_id/notes(.:format) notes#create # POST /:project_id/notes(.:format) notes#create
# project_note DELETE /:project_id/notes/:id(.:format) notes#destroy # project_note DELETE /:project_id/notes/:id(.:format) notes#destroy
describe Projects::NotesController, "routing" do describe Projects::NotesController, "routing" do
it "to #preview" do
post("/gitlab/gitlabhq/notes/preview").should route_to('projects/notes#preview', project_id: 'gitlab/gitlabhq')
end
it_behaves_like "RESTful project resources" do it_behaves_like "RESTful project resources" do
let(:actions) { [:index, :create, :destroy] } let(:actions) { [:index, :create, :destroy] }
let(:controller) { 'notes' } let(:controller) { 'notes' }
...@@ -415,6 +417,7 @@ describe Projects::BlobController, "routing" do ...@@ -415,6 +417,7 @@ describe Projects::BlobController, "routing" do
it "to #show" do it "to #show" do
get("/gitlab/gitlabhq/blob/master/app/models/project.rb").should route_to('projects/blob#show', project_id: 'gitlab/gitlabhq', id: 'master/app/models/project.rb') get("/gitlab/gitlabhq/blob/master/app/models/project.rb").should route_to('projects/blob#show', project_id: 'gitlab/gitlabhq', id: 'master/app/models/project.rb')
get("/gitlab/gitlabhq/blob/master/app/models/compare.rb").should route_to('projects/blob#show', project_id: 'gitlab/gitlabhq', id: 'master/app/models/compare.rb') get("/gitlab/gitlabhq/blob/master/app/models/compare.rb").should route_to('projects/blob#show', project_id: 'gitlab/gitlabhq', id: 'master/app/models/compare.rb')
get("/gitlab/gitlabhq/blob/master/app/models/diff.js").should route_to('projects/blob#show', project_id: 'gitlab/gitlabhq', id: 'master/app/models/diff.js')
get("/gitlab/gitlabhq/blob/master/files.scss").should route_to('projects/blob#show', project_id: 'gitlab/gitlabhq', id: 'master/files.scss') get("/gitlab/gitlabhq/blob/master/files.scss").should route_to('projects/blob#show', project_id: 'gitlab/gitlabhq', id: 'master/files.scss')
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