Commit 22ac0cc7ebc6411bdfd4df7e018ca0e1fd63c4c6
Exists in
master
and in
4 other branches
Merge branch 'feature/merge_requests'
Showing
29 changed files
with
531 additions
and
42 deletions
Show diff stats
app/assets/stylesheets/application.css
app/assets/stylesheets/issues.css.scss
| ... | ... | @@ -58,6 +58,8 @@ |
| 58 | 58 | padding: 0; |
| 59 | 59 | } |
| 60 | 60 | |
| 61 | +body.project-page .merge-request-form-holder table.no-borders tr, | |
| 62 | +body.project-page .merge-request-form-holder table.no-borders td, | |
| 61 | 63 | body.project-page .issue-form-holder table.no-borders tr, |
| 62 | 64 | body.project-page .issue-form-holder table.no-borders td, |
| 63 | 65 | body.project-page .new_snippet table tr, | ... | ... |
| ... | ... | @@ -0,0 +1,10 @@ |
| 1 | +// Place all the styles related to the MergeRequests controller here. | |
| 2 | +// They will automatically be included in application.css. | |
| 3 | +// You can use Sass (SCSS) here: http://sass-lang.com/ | |
| 4 | + | |
| 5 | + | |
| 6 | +.merge-request-form-holder { | |
| 7 | + select { | |
| 8 | + width:300px; | |
| 9 | + } | |
| 10 | +} | ... | ... |
app/assets/stylesheets/projects.css.scss
| ... | ... | @@ -155,6 +155,8 @@ input.ssh_project_url { |
| 155 | 155 | } |
| 156 | 156 | |
| 157 | 157 | /** FORM INPUTS **/ |
| 158 | +.new_merge_request, | |
| 159 | +.edit_merge_request, | |
| 158 | 160 | .user_new, |
| 159 | 161 | .new_key, |
| 160 | 162 | .new_issue, |
| ... | ... | @@ -326,12 +328,16 @@ body.project-page table .commit { |
| 326 | 328 | border:none; |
| 327 | 329 | text-shadow:none; |
| 328 | 330 | |
| 329 | - &.high { | |
| 331 | + &.inline { | |
| 332 | + display:inline; | |
| 333 | + } | |
| 334 | + | |
| 335 | + &.high, &.closed { | |
| 330 | 336 | background: #D12F19; |
| 331 | 337 | color:white; |
| 332 | 338 | } |
| 333 | 339 | |
| 334 | - &.today { | |
| 340 | + &.today, &.open { | |
| 335 | 341 | background: #44aa22; |
| 336 | 342 | color:white; |
| 337 | 343 | } |
| ... | ... | @@ -384,6 +390,32 @@ body.dashboard.project-page .news-feed .project-updates a.project-update span.up |
| 384 | 390 | body.dashboard.project-page .news-feed .project-updates a.project-update span.update-author strong{font-weight: bold; font-style: normal;} |
| 385 | 391 | /* eo Dashboard Page */ |
| 386 | 392 | |
| 393 | + | |
| 394 | +/** Merge requests */ | |
| 395 | +body.project-page .merge-request-commits {margin-bottom: 20px; display: block; width: 100%;} | |
| 396 | +body.project-page .merge-request-commits .data{ padding: 0} | |
| 397 | +body.project-page .merge-request-commits a.commit {padding: 10px; border-bottom: 1px solid #eee; overflow: hidden; display: block;} | |
| 398 | +body.project-page .merge-request-commits a.commit:last-child{border-bottom: 0} | |
| 399 | +body.project-page .merge-request-commits a.commit img{float: left; margin-right: 10px;} | |
| 400 | +body.project-page .merge-request-commits a.commit span.update-title, .dashboard-page .news-feed .project-updates li a span.update-author{display: block;} | |
| 401 | +body.project-page .merge-request-commits a.commit span.update-title{margin-bottom: 10px} | |
| 402 | +body.project-page .merge-request-commits a.commit span.update-author{color: #999; font-weight: normal; font-style: italic;} | |
| 403 | +body.project-page .merge-request-commits a.commit span.update-author strong{font-weight: bold; font-style: normal;} | |
| 404 | + | |
| 405 | + | |
| 406 | +/** Update entry **/ | |
| 407 | +.update-data { padding: 0 } | |
| 408 | +.update-data { width:100%; } | |
| 409 | +.update-data.ui-box .data { padding:0; } | |
| 410 | +a.update-item {padding: 10px; border-bottom: 1px solid #eee; overflow: hidden; display: block;} | |
| 411 | +a.update-item:last-child{border-bottom: 0} | |
| 412 | +a.update-item img{float: left; margin-right: 10px;} | |
| 413 | +a.update-item span.update-title, .dashboard-page .news-feed .project-updates li a span.update-author{display: block;} | |
| 414 | +a.update-item span.update-title{margin-bottom: 10px} | |
| 415 | +a.update-item span.update-author{color: #999; font-weight: normal; font-style: italic;} | |
| 416 | +a.update-item span.update-author strong{font-weight: bold; font-style: normal;} | |
| 417 | + | |
| 418 | + | |
| 387 | 419 | body.project-page .team_member_new .span-6, .team_member_edit .span-6{ padding:10px 0; } |
| 388 | 420 | |
| 389 | 421 | body.projects-page input.text.git-url.project_list_url { width:165px; } |
| ... | ... | @@ -394,3 +426,44 @@ body.project-page table.no-borders tr, |
| 394 | 426 | body.project-page table.no-borders td{ |
| 395 | 427 | border:none; |
| 396 | 428 | } |
| 429 | + | |
| 430 | +#gitlab-tabs { | |
| 431 | + .ui-tabs-nav { | |
| 432 | + border-bottom: 1px solid #DEDFE1; | |
| 433 | + | |
| 434 | + li { | |
| 435 | + background: none; | |
| 436 | + border:none; | |
| 437 | + font-size: 16px; | |
| 438 | + margin: 0; | |
| 439 | + padding: 0; | |
| 440 | + | |
| 441 | + a { | |
| 442 | + margin: 0; | |
| 443 | + padding: 10px 16px; | |
| 444 | + width:150px; | |
| 445 | + } | |
| 446 | + | |
| 447 | + &.ui-tabs-selected { | |
| 448 | + background-image: -webkit-gradient(linear, 0 0, 0 26, color-stop(0.076, #fefefe), to(#F6F7F8)); | |
| 449 | + background-image: -webkit-linear-gradient(#fefefe 7.6%, #F6F7F8); | |
| 450 | + background-image: -moz-linear-gradient(#fefefe 7.6%, #F6F7F8); | |
| 451 | + background-image: -o-linear-gradient(#fefefe 7.6%, #F6F7F8); | |
| 452 | + font-weight: bold; | |
| 453 | + border:1px solid #DEDFE1; | |
| 454 | + border-bottom: 1px solid #DEDFE1; | |
| 455 | + -webkit-border-top-left-radius: 5px; | |
| 456 | + -webkit-border-top-right-radius: 5px; | |
| 457 | + -moz-border-radius-topleft: 5px; | |
| 458 | + -moz-border-radius-topright: 5px; | |
| 459 | + border-top-left-radius: 5px; | |
| 460 | + border-top-right-radius: 5px; | |
| 461 | + } | |
| 462 | + } | |
| 463 | + } | |
| 464 | +} | |
| 465 | + | |
| 466 | +.ajax-tab-loading { | |
| 467 | + padding:40px; | |
| 468 | + display:none; | |
| 469 | +} | ... | ... |
| ... | ... | @@ -0,0 +1,90 @@ |
| 1 | +class MergeRequestsController < ApplicationController | |
| 2 | + before_filter :authenticate_user! | |
| 3 | + before_filter :project | |
| 4 | + before_filter :merge_request, :only => [:edit, :update, :destroy, :show, :commits, :diffs] | |
| 5 | + layout "project" | |
| 6 | + | |
| 7 | + # Authorize | |
| 8 | + before_filter :add_project_abilities | |
| 9 | + before_filter :authorize_read_project! | |
| 10 | + before_filter :authorize_write_project!, :only => [:new, :create, :edit, :update] | |
| 11 | + | |
| 12 | + def index | |
| 13 | + @merge_requests = @project.merge_requests | |
| 14 | + end | |
| 15 | + | |
| 16 | + def show | |
| 17 | + unless @project.repo.heads.map(&:name).include?(@merge_request.target_branch) && | |
| 18 | + @project.repo.heads.map(&:name).include?(@merge_request.source_branch) | |
| 19 | + head(404)and return | |
| 20 | + end | |
| 21 | + | |
| 22 | + @notes = @merge_request.notes.inc_author.order("created_at DESC").limit(20) | |
| 23 | + @note = @project.notes.new(:noteable => @merge_request) | |
| 24 | + | |
| 25 | + respond_to do |format| | |
| 26 | + format.html | |
| 27 | + format.js { respond_with_notes } | |
| 28 | + end | |
| 29 | + end | |
| 30 | + | |
| 31 | + def commits | |
| 32 | + @commits = @project.repo.commits_between(@merge_request.target_branch, @merge_request.source_branch).map {|c| Commit.new(c)} | |
| 33 | + render :template => "merge_requests/_commits", :layout => false | |
| 34 | + end | |
| 35 | + | |
| 36 | + def diffs | |
| 37 | + @commit = @project.commit(@merge_request.source_branch) | |
| 38 | + @diffs = @project.repo.diff(@merge_request.target_branch, @merge_request.source_branch) | |
| 39 | + render :template => "merge_requests/_diffs", :layout => false | |
| 40 | + end | |
| 41 | + | |
| 42 | + def new | |
| 43 | + @merge_request = @project.merge_requests.new | |
| 44 | + end | |
| 45 | + | |
| 46 | + def edit | |
| 47 | + end | |
| 48 | + | |
| 49 | + def create | |
| 50 | + @merge_request = @project.merge_requests.new(params[:merge_request]) | |
| 51 | + @merge_request.author = current_user | |
| 52 | + | |
| 53 | + respond_to do |format| | |
| 54 | + if @merge_request.save | |
| 55 | + format.html { redirect_to [@project, @merge_request], notice: 'Merge request was successfully created.' } | |
| 56 | + format.json { render json: @merge_request, status: :created, location: @merge_request } | |
| 57 | + else | |
| 58 | + format.html { render action: "new" } | |
| 59 | + format.json { render json: @merge_request.errors, status: :unprocessable_entity } | |
| 60 | + end | |
| 61 | + end | |
| 62 | + end | |
| 63 | + | |
| 64 | + def update | |
| 65 | + respond_to do |format| | |
| 66 | + if @merge_request.update_attributes(params[:merge_request]) | |
| 67 | + format.html { redirect_to [@project, @merge_request], notice: 'Merge request was successfully updated.' } | |
| 68 | + format.json { head :ok } | |
| 69 | + else | |
| 70 | + format.html { render action: "edit" } | |
| 71 | + format.json { render json: @merge_request.errors, status: :unprocessable_entity } | |
| 72 | + end | |
| 73 | + end | |
| 74 | + end | |
| 75 | + | |
| 76 | + def destroy | |
| 77 | + @merge_request.destroy | |
| 78 | + | |
| 79 | + respond_to do |format| | |
| 80 | + format.html { redirect_to project_merge_requests_url(@project) } | |
| 81 | + format.json { head :ok } | |
| 82 | + end | |
| 83 | + end | |
| 84 | + | |
| 85 | + protected | |
| 86 | + | |
| 87 | + def merge_request | |
| 88 | + @merge_request ||= @project.merge_requests.find(params[:id]) | |
| 89 | + end | |
| 90 | +end | ... | ... |
app/controllers/notes_controller.rb
| ... | ... | @@ -42,6 +42,8 @@ class NotesController < ApplicationController |
| 42 | 42 | Notify.note_commit_email(u, @note).deliver |
| 43 | 43 | when "Issue" then |
| 44 | 44 | Notify.note_issue_email(u, @note).deliver |
| 45 | + when "MergeRequest" | |
| 46 | + true # someone should write email notification | |
| 45 | 47 | when "Snippet" |
| 46 | 48 | true |
| 47 | 49 | else | ... | ... |
app/models/ability.rb
| ... | ... | @@ -17,6 +17,7 @@ class Ability |
| 17 | 17 | :read_issue, |
| 18 | 18 | :read_snippet, |
| 19 | 19 | :read_team_member, |
| 20 | + :read_merge_request, | |
| 20 | 21 | :read_note |
| 21 | 22 | ] if project.readers.include?(user) |
| 22 | 23 | |
| ... | ... | @@ -24,6 +25,7 @@ class Ability |
| 24 | 25 | :write_project, |
| 25 | 26 | :write_issue, |
| 26 | 27 | :write_snippet, |
| 28 | + :write_merge_request, | |
| 27 | 29 | :write_note |
| 28 | 30 | ] if project.writers.include?(user) |
| 29 | 31 | |
| ... | ... | @@ -32,6 +34,7 @@ class Ability |
| 32 | 34 | :admin_issue, |
| 33 | 35 | :admin_snippet, |
| 34 | 36 | :admin_team_member, |
| 37 | + :admin_merge_request, | |
| 35 | 38 | :admin_note |
| 36 | 39 | ] if project.admins.include?(user) |
| 37 | 40 | |
| ... | ... | @@ -39,7 +42,7 @@ class Ability |
| 39 | 42 | end |
| 40 | 43 | |
| 41 | 44 | class << self |
| 42 | - [:issue, :note, :snippet].each do |name| | |
| 45 | + [:issue, :note, :snippet, :merge_request].each do |name| | |
| 43 | 46 | define_method "#{name}_abilities" do |user, subject| |
| 44 | 47 | if subject.author == user |
| 45 | 48 | [ | ... | ... |
| ... | ... | @@ -0,0 +1,36 @@ |
| 1 | +class MergeRequest < ActiveRecord::Base | |
| 2 | + belongs_to :project | |
| 3 | + belongs_to :author, :class_name => "User" | |
| 4 | + belongs_to :assignee, :class_name => "User" | |
| 5 | + has_many :notes, :as => :noteable | |
| 6 | + | |
| 7 | + attr_protected :author, :author_id, :project, :project_id | |
| 8 | + | |
| 9 | + validates_presence_of :project_id | |
| 10 | + validates_presence_of :assignee_id | |
| 11 | + validates_presence_of :author_id | |
| 12 | + validates_presence_of :source_branch | |
| 13 | + validates_presence_of :target_branch | |
| 14 | + | |
| 15 | + delegate :name, | |
| 16 | + :email, | |
| 17 | + :to => :author, | |
| 18 | + :prefix => true | |
| 19 | + | |
| 20 | + delegate :name, | |
| 21 | + :email, | |
| 22 | + :to => :assignee, | |
| 23 | + :prefix => true | |
| 24 | + | |
| 25 | + validates :title, | |
| 26 | + :presence => true, | |
| 27 | + :length => { :within => 0..255 } | |
| 28 | + | |
| 29 | + scope :opened, where(:closed => false) | |
| 30 | + scope :closed, where(:closed => true) | |
| 31 | + scope :assigned, lambda { |u| where(:assignee_id => u.id)} | |
| 32 | + | |
| 33 | + def new? | |
| 34 | + today? && created_at == updated_at | |
| 35 | + end | |
| 36 | +end | ... | ... |
app/models/project.rb
| ... | ... | @@ -3,6 +3,7 @@ require "grit" |
| 3 | 3 | class Project < ActiveRecord::Base |
| 4 | 4 | belongs_to :owner, :class_name => "User" |
| 5 | 5 | |
| 6 | + has_many :merge_requests, :dependent => :destroy | |
| 6 | 7 | has_many :issues, :dependent => :destroy, :order => "position" |
| 7 | 8 | has_many :users_projects, :dependent => :destroy |
| 8 | 9 | has_many :users, :through => :users_projects | ... | ... |
app/views/issues/show.html.haml
| ... | ... | @@ -3,9 +3,9 @@ |
| 3 | 3 | = "Issue ##{@issue.id}" |
| 4 | 4 | .right |
| 5 | 5 | - if @issue.closed |
| 6 | - %span.tag.high Resolved | |
| 6 | + %span.tag.closed Closed | |
| 7 | 7 | - else |
| 8 | - %span.tag.today Open | |
| 8 | + %span.tag.open Open | |
| 9 | 9 | |
| 10 | 10 | .data |
| 11 | 11 | %p= @issue.title |
| ... | ... | @@ -28,7 +28,7 @@ |
| 28 | 28 | - if @issue.closed |
| 29 | 29 | = link_to 'Reopen', project_issue_path(@project, @issue, :issue => {:closed => false }, :status_only => true), :method => :put, :class => "grey-button" |
| 30 | 30 | - else |
| 31 | - = link_to 'Resolve', project_issue_path(@project, @issue, :issue => {:closed => true }, :status_only => true), :method => :put, :class => "grey-button" | |
| 31 | + = link_to 'Close', project_issue_path(@project, @issue, :issue => {:closed => true }, :status_only => true), :method => :put, :class => "grey-button" | |
| 32 | 32 | .right |
| 33 | 33 | = link_to 'Edit', edit_project_issue_path(@project, @issue), :class => "grey-button positive" |
| 34 | 34 | ... | ... |
app/views/layouts/project.html.haml
| ... | ... | @@ -39,6 +39,10 @@ |
| 39 | 39 | Wall |
| 40 | 40 | - if @project.common_notes.today.count > 0 |
| 41 | 41 | %span{ :class => "number" }= @project.common_notes.today.count |
| 42 | + = link_to project_merge_requests_path(@project), :class => (controller.controller_name == "merge_requests") ? "current" : nil do | |
| 43 | + Merge Requests | |
| 44 | + - if @project.merge_requests.opened.count > 0 | |
| 45 | + %span{ :class => "number" }= @project.merge_requests.opened.count | |
| 42 | 46 | = link_to project_snippets_path(@project), :class => (controller.controller_name == "snippets") ? "current" : nil do |
| 43 | 47 | Snippets |
| 44 | 48 | - if @project.snippets.non_expired.count > 0 | ... | ... |
| ... | ... | @@ -0,0 +1,17 @@ |
| 1 | +- if @commits.size > 0 | |
| 2 | + .merge-request-commits.ui-box.width-100p | |
| 3 | + - @commits.each do |commit| | |
| 4 | + %a{ :class => "commit", :href => project_commit_path(@project, :id => commit.id) } | |
| 5 | + - if commit.author_email | |
| 6 | + = image_tag gravatar_icon(commit.author_email), :class => "left", :width => 40, :style => "padding-right:5px;" | |
| 7 | + - else | |
| 8 | + = image_tag "no_avatar.png", :class => "left", :width => 40, :style => "padding-right:5px;" | |
| 9 | + %span.update-title | |
| 10 | + = truncate commit.safe_message, :length => 60 | |
| 11 | + %span.update-author | |
| 12 | + %strong= commit.author_name | |
| 13 | + authored | |
| 14 | + = time_ago_in_words(commit.created_at) | |
| 15 | + ago | |
| 16 | + .clear | |
| 17 | + | ... | ... |
| ... | ... | @@ -0,0 +1,22 @@ |
| 1 | +- @diffs.each do |diff| | |
| 2 | + - next if diff.diff.empty? | |
| 3 | + - file = (@commit.tree / diff.b_path) | |
| 4 | + - next unless file | |
| 5 | + .diff_file | |
| 6 | + .diff_file_header | |
| 7 | + - if diff.deleted_file | |
| 8 | + %strong{:id => "#{diff.b_path}"}= diff.a_path | |
| 9 | + - else | |
| 10 | + = link_to tree_file_project_ref_path(@project, @commit.id, diff.b_path) do | |
| 11 | + %strong{:id => "#{diff.b_path}"}= diff.b_path | |
| 12 | + %br/ | |
| 13 | + .diff_file_content | |
| 14 | + - if file.text? | |
| 15 | + = render :partial => "commits/text_file", :locals => { :diff => diff } | |
| 16 | + - elsif file.image? | |
| 17 | + .diff_file_content_image | |
| 18 | + %img{:src => "data:#{file.mime_type};base64,#{Base64.encode64(file.data)}"} | |
| 19 | + - else | |
| 20 | + %p | |
| 21 | + %center No preview for this file type | |
| 22 | + | ... | ... |
| ... | ... | @@ -0,0 +1,39 @@ |
| 1 | +%div.merge-request-form-holder | |
| 2 | + .ui-box.width-100p | |
| 3 | + %h3 | |
| 4 | + = @merge_request.new_record? ? "New Merge Request" : "Edit Merge Request ##{@merge_request.id}" | |
| 5 | + = form_for [@project, @merge_request] do |f| | |
| 6 | + .data | |
| 7 | + %table.no-borders | |
| 8 | + -if @merge_request.errors.any? | |
| 9 | + %tr | |
| 10 | + %td Errors | |
| 11 | + %td | |
| 12 | + #error_explanation | |
| 13 | + - @merge_request.errors.full_messages.each do |msg| | |
| 14 | + %span= msg | |
| 15 | + %br | |
| 16 | + | |
| 17 | + %tr | |
| 18 | + %td= f.label :title | |
| 19 | + %td= f.text_field :title | |
| 20 | + %tr | |
| 21 | + %td= f.label :source_branch, "From" | |
| 22 | + %td= f.select(:source_branch, @project.heads.map(&:name), { :include_blank => "Select branch" }) | |
| 23 | + %tr | |
| 24 | + %td= f.label :target_branch, "To" | |
| 25 | + %td= f.select(:target_branch, @project.heads.map(&:name), { :include_blank => "Select branch" }) | |
| 26 | + %tr | |
| 27 | + %td= f.label :assignee_id, "Assign to" | |
| 28 | + %td= f.select(:assignee_id, @project.users.all.collect {|p| [ p.name, p.id ] }, { :include_blank => "Select user" }) | |
| 29 | + .buttons | |
| 30 | + = f.submit 'Save', :class => "grey-button" | |
| 31 | + .right= link_to 'Back', project_merge_requests_path(@project), :class => "grey-button" | |
| 32 | + | |
| 33 | +:javascript | |
| 34 | + $(function(){ | |
| 35 | + $('select#merge_request_assignee_id').chosen(); | |
| 36 | + $('select#merge_request_source_branch').chosen(); | |
| 37 | + $('select#merge_request_target_branch').chosen(); | |
| 38 | + }); | |
| 39 | + | ... | ... |
| ... | ... | @@ -0,0 +1,14 @@ |
| 1 | +%a.update-item{:href => project_merge_request_path(@project, merge_request)} | |
| 2 | + = image_tag gravatar_icon(merge_request.author_email), :class => "left", :width => 40 | |
| 3 | + %span.update-title | |
| 4 | + = merge_request.title | |
| 5 | + %span.update-author | |
| 6 | + %strong= merge_request.author_name | |
| 7 | + authored | |
| 8 | + = time_ago_in_words(merge_request.created_at) | |
| 9 | + ago | |
| 10 | + .right | |
| 11 | + %span.tag.commit= merge_request.source_branch | |
| 12 | + → | |
| 13 | + %span.tag.commit= merge_request.target_branch | |
| 14 | + | ... | ... |
| ... | ... | @@ -0,0 +1 @@ |
| 1 | += render 'form' | ... | ... |
| ... | ... | @@ -0,0 +1,20 @@ |
| 1 | +- if @merge_requests.opened.count > 0 | |
| 2 | + %div{ :class => "update-data ui-box ui-box-small ui-box-big" } | |
| 3 | + %h3 | |
| 4 | + %span.tag.open Open | |
| 5 | + .data | |
| 6 | + = render @merge_requests.opened | |
| 7 | + | |
| 8 | + .clear | |
| 9 | + %br | |
| 10 | + | |
| 11 | +- if @merge_requests.closed.count > 0 | |
| 12 | + %div{ :class => "update-data ui-box ui-box-small ui-box-big" } | |
| 13 | + %h3 | |
| 14 | + %span.tag.closed Closed | |
| 15 | + .data | |
| 16 | + = render @merge_requests.closed | |
| 17 | + .clear | |
| 18 | + %br | |
| 19 | + | |
| 20 | += link_to 'New Merge request', new_project_merge_request_path(@project), :class => "grey-button" | ... | ... |
| ... | ... | @@ -0,0 +1 @@ |
| 1 | += render 'form' | ... | ... |
| ... | ... | @@ -0,0 +1,59 @@ |
| 1 | +.merge-request-show-holder.ui-box.width-100p | |
| 2 | + %h3 | |
| 3 | + = "Merge Request ##{@merge_request.id}:" | |
| 4 | + | |
| 5 | + .tag.commit.inline= @merge_request.source_branch | |
| 6 | + → | |
| 7 | + .tag.commit.inline= @merge_request.target_branch | |
| 8 | + .right | |
| 9 | + - if @merge_request.closed | |
| 10 | + %span.tag.high Closed | |
| 11 | + - else | |
| 12 | + %span.tag.today Open | |
| 13 | + | |
| 14 | + .data | |
| 15 | + %p= @merge_request.title | |
| 16 | + | |
| 17 | + - if @merge_request.author == @merge_request.assignee | |
| 18 | + = image_tag gravatar_icon(@merge_request.assignee_email), :width => 20, :style => "padding:0 5px;" | |
| 19 | + = @merge_request.assignee_name | |
| 20 | + - else | |
| 21 | + = image_tag gravatar_icon(@merge_request.author_email), :width => 20, :style => "padding:0 5px;" | |
| 22 | + = @merge_request.author_name | |
| 23 | + → | |
| 24 | + = image_tag gravatar_icon(@merge_request.assignee_email), :width => 20, :style => "padding:0 5px;" | |
| 25 | + = @merge_request.assignee_name | |
| 26 | + .right | |
| 27 | + %cite.cgray= @merge_request.created_at.stamp("21 Aug 2011, 11:15pm") | |
| 28 | + .clear | |
| 29 | + | |
| 30 | + .buttons | |
| 31 | + - if can? current_user, :write_project, @project | |
| 32 | + - if @merge_request.closed | |
| 33 | + = link_to 'Reopen', project_merge_request_path(@project, @merge_request, :merge_request => {:closed => false }, :status_only => true), :method => :put, :class => "grey-button" | |
| 34 | + - else | |
| 35 | + = link_to 'Close', project_merge_request_path(@project, @merge_request, :merge_request => {:closed => true }, :status_only => true), :method => :put, :class => "grey-button" | |
| 36 | + .right | |
| 37 | + = link_to 'Edit', edit_project_merge_request_path(@project, @merge_request), :class => "grey-button positive" | |
| 38 | + | |
| 39 | +.clear | |
| 40 | +%br | |
| 41 | +%br | |
| 42 | + | |
| 43 | +#gitlab-tabs | |
| 44 | + %ul | |
| 45 | + %li= link_to "Notes", "#merge-notes" | |
| 46 | + %li= link_to "Commits", commits_project_merge_request_path(@project, @merge_request) | |
| 47 | + %li= link_to "Diff", diffs_project_merge_request_path(@project, @merge_request) | |
| 48 | + | |
| 49 | + #merge-notes | |
| 50 | + .issue_notes= render "notes/notes" | |
| 51 | + .loading{ :style => "display:none;"} | |
| 52 | + %center= image_tag "ajax-loader.gif" | |
| 53 | + .clear | |
| 54 | + | |
| 55 | + | |
| 56 | +:javascript | |
| 57 | + $(function(){ | |
| 58 | + $("#gitlab-tabs").tabs(); | |
| 59 | + }) | ... | ... |
config/routes.rb
| ... | ... | @@ -0,0 +1,15 @@ |
| 1 | +class CreateMergeRequests < ActiveRecord::Migration | |
| 2 | + def change | |
| 3 | + create_table :merge_requests do |t| | |
| 4 | + t.string :target_branch, :null => false | |
| 5 | + t.string :source_branch, :null => false | |
| 6 | + t.integer :project_id, :null => false | |
| 7 | + t.integer :author_id | |
| 8 | + t.integer :assignee_id | |
| 9 | + t.string :title | |
| 10 | + t.boolean :closed, :default => false, :null => false | |
| 11 | + | |
| 12 | + t.timestamps | |
| 13 | + end | |
| 14 | + end | |
| 15 | +end | ... | ... |
db/schema.rb
| ... | ... | @@ -11,7 +11,7 @@ |
| 11 | 11 | # |
| 12 | 12 | # It's strongly recommended to check this file into your version control system. |
| 13 | 13 | |
| 14 | -ActiveRecord::Schema.define(:version => 20111124115339) do | |
| 14 | +ActiveRecord::Schema.define(:version => 20111127155345) do | |
| 15 | 15 | |
| 16 | 16 | create_table "features", :force => true do |t| |
| 17 | 17 | t.string "name" |
| ... | ... | @@ -21,6 +21,8 @@ ActiveRecord::Schema.define(:version => 20111124115339) do |
| 21 | 21 | t.integer "project_id" |
| 22 | 22 | t.datetime "created_at" |
| 23 | 23 | t.datetime "updated_at" |
| 24 | + t.string "version" | |
| 25 | + t.integer "status", :default => 0, :null => false | |
| 24 | 26 | end |
| 25 | 27 | |
| 26 | 28 | create_table "issues", :force => true do |t| |
| ... | ... | @@ -45,6 +47,18 @@ ActiveRecord::Schema.define(:version => 20111124115339) do |
| 45 | 47 | t.string "identifier" |
| 46 | 48 | end |
| 47 | 49 | |
| 50 | + create_table "merge_requests", :force => true do |t| | |
| 51 | + t.string "target_branch", :null => false | |
| 52 | + t.string "source_branch", :null => false | |
| 53 | + t.integer "project_id", :null => false | |
| 54 | + t.integer "author_id" | |
| 55 | + t.integer "assignee_id" | |
| 56 | + t.string "title" | |
| 57 | + t.boolean "closed", :default => false, :null => false | |
| 58 | + t.datetime "created_at" | |
| 59 | + t.datetime "updated_at" | |
| 60 | + end | |
| 61 | + | |
| 48 | 62 | create_table "notes", :force => true do |t| |
| 49 | 63 | t.text "note" |
| 50 | 64 | t.string "noteable_id" | ... | ... |
spec/factories.rb
| ... | ... | @@ -34,6 +34,12 @@ Factory.add(:issue, Issue) do |obj| |
| 34 | 34 | obj.title = Faker::Lorem.sentence |
| 35 | 35 | end |
| 36 | 36 | |
| 37 | +Factory.add(:merge_request, MergeRequest) do |obj| | |
| 38 | + obj.title = Faker::Lorem.sentence | |
| 39 | + obj.source_branch = "master" | |
| 40 | + obj.target_branch = "master" | |
| 41 | +end | |
| 42 | + | |
| 37 | 43 | Factory.add(:snippet, Snippet) do |obj| |
| 38 | 44 | obj.title = Faker::Lorem.sentence |
| 39 | 45 | obj.file_name = Faker::Lorem.sentence | ... | ... |
spec/helpers/application_helper_spec.rb
| ... | ... | @@ -1,35 +0,0 @@ |
| 1 | -require 'spec_helper' | |
| 2 | - | |
| 3 | -describe ApplicationHelper do | |
| 4 | - context ".gravatar_icon" do | |
| 5 | - context "over http" do | |
| 6 | - it "returns the correct URL to www.gravatar.com" do | |
| 7 | - expected = "http://www.gravatar.com/avatar/f7daa65b2aa96290bb47c4d68d11fe6a?s=40&d=identicon" | |
| 8 | - | |
| 9 | - # Pretend we're running over HTTP | |
| 10 | - helper.stub(:request) do | |
| 11 | - request = double('request') | |
| 12 | - request.stub(:ssl?) { false } | |
| 13 | - request | |
| 14 | - end | |
| 15 | - | |
| 16 | - helper.gravatar_icon("admin@local.host").should == expected | |
| 17 | - end | |
| 18 | - end | |
| 19 | - | |
| 20 | - context "over https" do | |
| 21 | - it "returns the correct URL to secure.gravatar.com" do | |
| 22 | - expected = "https://secure.gravatar.com/avatar/f7daa65b2aa96290bb47c4d68d11fe6a?s=40&d=identicon" | |
| 23 | - | |
| 24 | - # Pretend we're running over HTTPS | |
| 25 | - helper.stub(:request) do | |
| 26 | - request = double('request') | |
| 27 | - request.stub(:ssl?) { true } | |
| 28 | - request | |
| 29 | - end | |
| 30 | - | |
| 31 | - helper.gravatar_icon("admin@local.host").should == expected | |
| 32 | - end | |
| 33 | - end | |
| 34 | - end | |
| 35 | -end |
| ... | ... | @@ -0,0 +1,69 @@ |
| 1 | +require 'spec_helper' | |
| 2 | + | |
| 3 | +describe "MergeRequests" do | |
| 4 | + let(:project) { Factory :project } | |
| 5 | + | |
| 6 | + before do | |
| 7 | + login_as :user | |
| 8 | + project.add_access(@user, :read, :write) | |
| 9 | + @merge_request = Factory :merge_request, | |
| 10 | + :author => @user, | |
| 11 | + :assignee => @user, | |
| 12 | + :project => project | |
| 13 | + end | |
| 14 | + | |
| 15 | + describe "GET /merge_requests" do | |
| 16 | + before do | |
| 17 | + visit project_merge_requests_path(project) | |
| 18 | + end | |
| 19 | + | |
| 20 | + subject { page } | |
| 21 | + | |
| 22 | + it { should have_content(@merge_request.title) } | |
| 23 | + it { should have_content(@merge_request.target_branch) } | |
| 24 | + it { should have_content(@merge_request.source_branch) } | |
| 25 | + it { should have_content(@merge_request.assignee.name) } | |
| 26 | + end | |
| 27 | + | |
| 28 | + describe "GET /merge_request/:id" do | |
| 29 | + before do | |
| 30 | + visit project_merge_request_path(project, @merge_request) | |
| 31 | + end | |
| 32 | + | |
| 33 | + subject { page } | |
| 34 | + | |
| 35 | + it { should have_content(@merge_request.title) } | |
| 36 | + it { should have_content(@merge_request.target_branch) } | |
| 37 | + it { should have_content(@merge_request.source_branch) } | |
| 38 | + it { should have_content(@merge_request.assignee.name) } | |
| 39 | + | |
| 40 | + describe "Close merge request" do | |
| 41 | + before { click_link "Close" } | |
| 42 | + | |
| 43 | + it { should have_content(@merge_request.title) } | |
| 44 | + it "Show page should inform user that merge request closed" do | |
| 45 | + within ".merge-request-show-holder h3" do | |
| 46 | + page.should have_content "Closed" | |
| 47 | + end | |
| 48 | + end | |
| 49 | + end | |
| 50 | + end | |
| 51 | + | |
| 52 | + describe "GET /merge_requests/new" do | |
| 53 | + before do | |
| 54 | + visit new_project_merge_request_path(project) | |
| 55 | + fill_in "merge_request_title", :with => "Merge Request Title" | |
| 56 | + select "master", :from => "merge_request_source_branch" | |
| 57 | + select "master", :from => "merge_request_target_branch" | |
| 58 | + select @user.name, :from => "merge_request_assignee_id" | |
| 59 | + click_button "Save" | |
| 60 | + end | |
| 61 | + | |
| 62 | + it { current_path.should == project_merge_request_path(project, project.merge_requests.last) } | |
| 63 | + | |
| 64 | + it "should create merge request" do | |
| 65 | + page.should have_content "Open" | |
| 66 | + page.should have_content @user.name | |
| 67 | + end | |
| 68 | + end | |
| 69 | +end | ... | ... |
spec/requests/projects_security_spec.rb
| ... | ... | @@ -122,5 +122,14 @@ describe "Projects" do |
| 122 | 122 | it { project_snippets_path(@project).should be_denied_for :user } |
| 123 | 123 | it { project_snippets_path(@project).should be_denied_for :visitor } |
| 124 | 124 | end |
| 125 | + | |
| 126 | + describe "GET /project_code/merge_requests" do | |
| 127 | + it { project_merge_requests_path(@project).should be_allowed_for @u1 } | |
| 128 | + it { project_merge_requests_path(@project).should be_allowed_for @u3 } | |
| 129 | + it { project_merge_requests_path(@project).should be_denied_for :admin } | |
| 130 | + it { project_merge_requests_path(@project).should be_denied_for @u2 } | |
| 131 | + it { project_merge_requests_path(@project).should be_denied_for :user } | |
| 132 | + it { project_merge_requests_path(@project).should be_denied_for :visitor } | |
| 133 | + end | |
| 125 | 134 | end |
| 126 | 135 | end | ... | ... |