Commit 22ac0cc7ebc6411bdfd4df7e018ca0e1fd63c4c6

Authored by Dmitriy Zaporozhets
2 parents 7002b9ff e74ec3c9

Merge branch 'feature/merge_requests'

app/assets/javascripts/merge_requests.js.coffee 0 → 100644
@@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
  1 +# Place all the behaviors and hooks related to the matching controller here.
  2 +# All this logic will automatically be available in application.js.
  3 +# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/
app/assets/stylesheets/application.css
@@ -49,3 +49,4 @@ @@ -49,3 +49,4 @@
49 .no-padding { 49 .no-padding {
50 padding:0 !important; 50 padding:0 !important;
51 } 51 }
  52 +
app/assets/stylesheets/issues.css.scss
@@ -58,6 +58,8 @@ @@ -58,6 +58,8 @@
58 padding: 0; 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 body.project-page .issue-form-holder table.no-borders tr, 63 body.project-page .issue-form-holder table.no-borders tr,
62 body.project-page .issue-form-holder table.no-borders td, 64 body.project-page .issue-form-holder table.no-borders td,
63 body.project-page .new_snippet table tr, 65 body.project-page .new_snippet table tr,
app/assets/stylesheets/merge_requests.css.scss 0 → 100644
@@ -0,0 +1,10 @@ @@ -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,6 +155,8 @@ input.ssh_project_url {
155 } 155 }
156 156
157 /** FORM INPUTS **/ 157 /** FORM INPUTS **/
  158 +.new_merge_request,
  159 +.edit_merge_request,
158 .user_new, 160 .user_new,
159 .new_key, 161 .new_key,
160 .new_issue, 162 .new_issue,
@@ -326,12 +328,16 @@ body.project-page table .commit { @@ -326,12 +328,16 @@ body.project-page table .commit {
326 border:none; 328 border:none;
327 text-shadow:none; 329 text-shadow:none;
328 330
329 - &.high { 331 + &.inline {
  332 + display:inline;
  333 + }
  334 +
  335 + &.high, &.closed {
330 background: #D12F19; 336 background: #D12F19;
331 color:white; 337 color:white;
332 } 338 }
333 339
334 - &.today { 340 + &.today, &.open {
335 background: #44aa22; 341 background: #44aa22;
336 color:white; 342 color:white;
337 } 343 }
@@ -384,6 +390,32 @@ body.dashboard.project-page .news-feed .project-updates a.project-update span.up @@ -384,6 +390,32 @@ body.dashboard.project-page .news-feed .project-updates a.project-update span.up
384 body.dashboard.project-page .news-feed .project-updates a.project-update span.update-author strong{font-weight: bold; font-style: normal;} 390 body.dashboard.project-page .news-feed .project-updates a.project-update span.update-author strong{font-weight: bold; font-style: normal;}
385 /* eo Dashboard Page */ 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 body.project-page .team_member_new .span-6, .team_member_edit .span-6{ padding:10px 0; } 419 body.project-page .team_member_new .span-6, .team_member_edit .span-6{ padding:10px 0; }
388 420
389 body.projects-page input.text.git-url.project_list_url { width:165px; } 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,3 +426,44 @@ body.project-page table.no-borders tr,
394 body.project-page table.no-borders td{ 426 body.project-page table.no-borders td{
395 border:none; 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 +}
app/controllers/merge_requests_controller.rb 0 → 100644
@@ -0,0 +1,90 @@ @@ -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 &lt; ApplicationController @@ -42,6 +42,8 @@ class NotesController &lt; ApplicationController
42 Notify.note_commit_email(u, @note).deliver 42 Notify.note_commit_email(u, @note).deliver
43 when "Issue" then 43 when "Issue" then
44 Notify.note_issue_email(u, @note).deliver 44 Notify.note_issue_email(u, @note).deliver
  45 + when "MergeRequest"
  46 + true # someone should write email notification
45 when "Snippet" 47 when "Snippet"
46 true 48 true
47 else 49 else
app/helpers/merge_requests_helper.rb 0 → 100644
@@ -0,0 +1,2 @@ @@ -0,0 +1,2 @@
  1 +module MergeRequestsHelper
  2 +end
app/models/ability.rb
@@ -17,6 +17,7 @@ class Ability @@ -17,6 +17,7 @@ class Ability
17 :read_issue, 17 :read_issue,
18 :read_snippet, 18 :read_snippet,
19 :read_team_member, 19 :read_team_member,
  20 + :read_merge_request,
20 :read_note 21 :read_note
21 ] if project.readers.include?(user) 22 ] if project.readers.include?(user)
22 23
@@ -24,6 +25,7 @@ class Ability @@ -24,6 +25,7 @@ class Ability
24 :write_project, 25 :write_project,
25 :write_issue, 26 :write_issue,
26 :write_snippet, 27 :write_snippet,
  28 + :write_merge_request,
27 :write_note 29 :write_note
28 ] if project.writers.include?(user) 30 ] if project.writers.include?(user)
29 31
@@ -32,6 +34,7 @@ class Ability @@ -32,6 +34,7 @@ class Ability
32 :admin_issue, 34 :admin_issue,
33 :admin_snippet, 35 :admin_snippet,
34 :admin_team_member, 36 :admin_team_member,
  37 + :admin_merge_request,
35 :admin_note 38 :admin_note
36 ] if project.admins.include?(user) 39 ] if project.admins.include?(user)
37 40
@@ -39,7 +42,7 @@ class Ability @@ -39,7 +42,7 @@ class Ability
39 end 42 end
40 43
41 class << self 44 class << self
42 - [:issue, :note, :snippet].each do |name| 45 + [:issue, :note, :snippet, :merge_request].each do |name|
43 define_method "#{name}_abilities" do |user, subject| 46 define_method "#{name}_abilities" do |user, subject|
44 if subject.author == user 47 if subject.author == user
45 [ 48 [
app/models/merge_request.rb 0 → 100644
@@ -0,0 +1,36 @@ @@ -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 &quot;grit&quot; @@ -3,6 +3,7 @@ require &quot;grit&quot;
3 class Project < ActiveRecord::Base 3 class Project < ActiveRecord::Base
4 belongs_to :owner, :class_name => "User" 4 belongs_to :owner, :class_name => "User"
5 5
  6 + has_many :merge_requests, :dependent => :destroy
6 has_many :issues, :dependent => :destroy, :order => "position" 7 has_many :issues, :dependent => :destroy, :order => "position"
7 has_many :users_projects, :dependent => :destroy 8 has_many :users_projects, :dependent => :destroy
8 has_many :users, :through => :users_projects 9 has_many :users, :through => :users_projects
app/views/issues/show.html.haml
@@ -3,9 +3,9 @@ @@ -3,9 +3,9 @@
3 = "Issue ##{@issue.id}" 3 = "Issue ##{@issue.id}"
4 .right 4 .right
5 - if @issue.closed 5 - if @issue.closed
6 - %span.tag.high Resolved 6 + %span.tag.closed Closed
7 - else 7 - else
8 - %span.tag.today Open 8 + %span.tag.open Open
9 9
10 .data 10 .data
11 %p= @issue.title 11 %p= @issue.title
@@ -28,7 +28,7 @@ @@ -28,7 +28,7 @@
28 - if @issue.closed 28 - if @issue.closed
29 = link_to 'Reopen', project_issue_path(@project, @issue, :issue => {:closed => false }, :status_only => true), :method => :put, :class => "grey-button" 29 = link_to 'Reopen', project_issue_path(@project, @issue, :issue => {:closed => false }, :status_only => true), :method => :put, :class => "grey-button"
30 - else 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 .right 32 .right
33 = link_to 'Edit', edit_project_issue_path(@project, @issue), :class => "grey-button positive" 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,6 +39,10 @@
39 Wall 39 Wall
40 - if @project.common_notes.today.count > 0 40 - if @project.common_notes.today.count > 0
41 %span{ :class => "number" }= @project.common_notes.today.count 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 = link_to project_snippets_path(@project), :class => (controller.controller_name == "snippets") ? "current" : nil do 46 = link_to project_snippets_path(@project), :class => (controller.controller_name == "snippets") ? "current" : nil do
43 Snippets 47 Snippets
44 - if @project.snippets.non_expired.count > 0 48 - if @project.snippets.non_expired.count > 0
app/views/merge_requests/_commits.html.haml 0 → 100644
@@ -0,0 +1,17 @@ @@ -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 +
app/views/merge_requests/_diffs.html.haml 0 → 100644
@@ -0,0 +1,22 @@ @@ -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 +
app/views/merge_requests/_form.html.haml 0 → 100644
@@ -0,0 +1,39 @@ @@ -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 +
app/views/merge_requests/_merge_request.html.haml 0 → 100644
@@ -0,0 +1,14 @@ @@ -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 + &rarr;
  13 + %span.tag.commit= merge_request.target_branch
  14 +
app/views/merge_requests/edit.html.haml 0 → 100644
@@ -0,0 +1 @@ @@ -0,0 +1 @@
  1 += render 'form'
app/views/merge_requests/index.html.haml 0 → 100644
@@ -0,0 +1,20 @@ @@ -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"
app/views/merge_requests/new.html.haml 0 → 100644
@@ -0,0 +1 @@ @@ -0,0 +1 @@
  1 += render 'form'
app/views/merge_requests/show.html.haml 0 → 100644
@@ -0,0 +1,59 @@ @@ -0,0 +1,59 @@
  1 +.merge-request-show-holder.ui-box.width-100p
  2 + %h3
  3 + = "Merge Request ##{@merge_request.id}:"
  4 + &nbsp;
  5 + .tag.commit.inline= @merge_request.source_branch
  6 + &rarr;
  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 + &rarr;
  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
@@ -59,6 +59,12 @@ Gitlab::Application.routes.draw do @@ -59,6 +59,12 @@ Gitlab::Application.routes.draw do
59 end 59 end
60 end 60 end
61 61
  62 + resources :merge_requests do
  63 + member do
  64 + get :diffs
  65 + get :commits
  66 + end
  67 + end
62 resources :snippets 68 resources :snippets
63 resources :commits 69 resources :commits
64 resources :team_members 70 resources :team_members
db/migrate/20111127155345_create_merge_requests.rb 0 → 100644
@@ -0,0 +1,15 @@ @@ -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
@@ -11,7 +11,7 @@ @@ -11,7 +11,7 @@
11 # 11 #
12 # It's strongly recommended to check this file into your version control system. 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 create_table "features", :force => true do |t| 16 create_table "features", :force => true do |t|
17 t.string "name" 17 t.string "name"
@@ -21,6 +21,8 @@ ActiveRecord::Schema.define(:version =&gt; 20111124115339) do @@ -21,6 +21,8 @@ ActiveRecord::Schema.define(:version =&gt; 20111124115339) do
21 t.integer "project_id" 21 t.integer "project_id"
22 t.datetime "created_at" 22 t.datetime "created_at"
23 t.datetime "updated_at" 23 t.datetime "updated_at"
  24 + t.string "version"
  25 + t.integer "status", :default => 0, :null => false
24 end 26 end
25 27
26 create_table "issues", :force => true do |t| 28 create_table "issues", :force => true do |t|
@@ -45,6 +47,18 @@ ActiveRecord::Schema.define(:version =&gt; 20111124115339) do @@ -45,6 +47,18 @@ ActiveRecord::Schema.define(:version =&gt; 20111124115339) do
45 t.string "identifier" 47 t.string "identifier"
46 end 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 create_table "notes", :force => true do |t| 62 create_table "notes", :force => true do |t|
49 t.text "note" 63 t.text "note"
50 t.string "noteable_id" 64 t.string "noteable_id"
spec/factories.rb
@@ -34,6 +34,12 @@ Factory.add(:issue, Issue) do |obj| @@ -34,6 +34,12 @@ Factory.add(:issue, Issue) do |obj|
34 obj.title = Faker::Lorem.sentence 34 obj.title = Faker::Lorem.sentence
35 end 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 Factory.add(:snippet, Snippet) do |obj| 43 Factory.add(:snippet, Snippet) do |obj|
38 obj.title = Faker::Lorem.sentence 44 obj.title = Faker::Lorem.sentence
39 obj.file_name = Faker::Lorem.sentence 45 obj.file_name = Faker::Lorem.sentence
spec/helpers/application_helper_spec.rb
@@ -1,35 +0,0 @@ @@ -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  
spec/models/merge_request_spec.rb 0 → 100644
@@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
  1 +require 'spec_helper'
  2 +
  3 +describe MergeRequest do
  4 + pending "add some examples to (or delete) #{__FILE__}"
  5 +end
spec/requests/merge_requests_spec.rb 0 → 100644
@@ -0,0 +1,69 @@ @@ -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 &quot;Projects&quot; do @@ -122,5 +122,14 @@ describe &quot;Projects&quot; do
122 it { project_snippets_path(@project).should be_denied_for :user } 122 it { project_snippets_path(@project).should be_denied_for :user }
123 it { project_snippets_path(@project).should be_denied_for :visitor } 123 it { project_snippets_path(@project).should be_denied_for :visitor }
124 end 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 end 134 end
126 end 135 end