Commit 6d460aa2d6b3959593c168eed181516036525393
1 parent
22442039
Exists in
master
and in
4 other branches
merge request entity
Showing
21 changed files
with
322 additions
and
37 deletions
Show diff stats
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, |
... | ... | @@ -384,6 +386,19 @@ body.dashboard.project-page .news-feed .project-updates a.project-update span.up |
384 | 386 | body.dashboard.project-page .news-feed .project-updates a.project-update span.update-author strong{font-weight: bold; font-style: normal;} |
385 | 387 | /* eo Dashboard Page */ |
386 | 388 | |
389 | + | |
390 | +/** Merge requests */ | |
391 | +body.project-page .merge-request-commits {margin-bottom: 20px; display: block; width: 100%;} | |
392 | +body.project-page .merge-request-commits .data{ padding: 0} | |
393 | +body.project-page .merge-request-commits a.commit {padding: 10px; border-bottom: 1px solid #eee; overflow: hidden; display: block;} | |
394 | +body.project-page .merge-request-commits a.commit:last-child{border-bottom: 0} | |
395 | +body.project-page .merge-request-commits a.commit img{float: left; margin-right: 10px;} | |
396 | +body.project-page .merge-request-commits a.commit span.update-title, .dashboard-page .news-feed .project-updates li a span.update-author{display: block;} | |
397 | +body.project-page .merge-request-commits a.commit span.update-title{margin-bottom: 10px} | |
398 | +body.project-page .merge-request-commits a.commit span.update-author{color: #999; font-weight: normal; font-style: italic;} | |
399 | +body.project-page .merge-request-commits a.commit span.update-author strong{font-weight: bold; font-style: normal;} | |
400 | + | |
401 | + | |
387 | 402 | body.project-page .team_member_new .span-6, .team_member_edit .span-6{ padding:10px 0; } |
388 | 403 | |
389 | 404 | body.projects-page input.text.git-url.project_list_url { width:165px; } | ... | ... |
... | ... | @@ -0,0 +1,73 @@ |
1 | +class MergeRequestsController < ApplicationController | |
2 | + before_filter :authenticate_user! | |
3 | + before_filter :project | |
4 | + before_filter :merge_request, :only => [:edit, :update, :destroy, :show] | |
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.all | |
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 | + @commits = @project.repo.commits_between(@merge_request.target_branch, @merge_request.source_branch).map {|c| Commit.new(c)} | |
23 | + end | |
24 | + | |
25 | + def new | |
26 | + @merge_request = @project.merge_requests.new | |
27 | + end | |
28 | + | |
29 | + def edit | |
30 | + end | |
31 | + | |
32 | + def create | |
33 | + @merge_request = @project.merge_requests.new(params[:merge_request]) | |
34 | + @merge_request.author = current_user | |
35 | + | |
36 | + respond_to do |format| | |
37 | + if @merge_request.save | |
38 | + format.html { redirect_to [@project, @merge_request], notice: 'Merge request was successfully created.' } | |
39 | + format.json { render json: @merge_request, status: :created, location: @merge_request } | |
40 | + else | |
41 | + format.html { render action: "new" } | |
42 | + format.json { render json: @merge_request.errors, status: :unprocessable_entity } | |
43 | + end | |
44 | + end | |
45 | + end | |
46 | + | |
47 | + def update | |
48 | + respond_to do |format| | |
49 | + if @merge_request.update_attributes(params[:merge_request]) | |
50 | + format.html { redirect_to [@project, @merge_request], notice: 'Merge request was successfully updated.' } | |
51 | + format.json { head :ok } | |
52 | + else | |
53 | + format.html { render action: "edit" } | |
54 | + format.json { render json: @merge_request.errors, status: :unprocessable_entity } | |
55 | + end | |
56 | + end | |
57 | + end | |
58 | + | |
59 | + def destroy | |
60 | + @merge_request.destroy | |
61 | + | |
62 | + respond_to do |format| | |
63 | + format.html { redirect_to project_merge_requests_url(@project) } | |
64 | + format.json { head :ok } | |
65 | + end | |
66 | + end | |
67 | + | |
68 | + protected | |
69 | + | |
70 | + def merge_request | |
71 | + @merge_request ||= @project.merge_requests.find(params[:id]) | |
72 | + end | |
73 | +end | ... | ... |
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/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,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 :target_branch, "From" | |
22 | + %td= f.select(:target_branch, @project.heads.map(&:name), { :include_blank => "Select branch" }) | |
23 | + %tr | |
24 | + %td= f.label :source_branch, "To" | |
25 | + %td= f.select(:source_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 @@ |
1 | += render 'form' | ... | ... |
... | ... | @@ -0,0 +1,25 @@ |
1 | +%table | |
2 | + %thead | |
3 | + %th Target branch | |
4 | + %th Source branch | |
5 | + %th Author | |
6 | + %th Assignee | |
7 | + %th Title | |
8 | + %th | |
9 | + %th | |
10 | + %th | |
11 | + | |
12 | + - @merge_requests.each do |merge_request| | |
13 | + %tr | |
14 | + %td= merge_request.target_branch | |
15 | + %td= merge_request.source_branch | |
16 | + %td= merge_request.author_id | |
17 | + %td= merge_request.assignee_id | |
18 | + %td= merge_request.title | |
19 | + %td= link_to 'Show', [@project, merge_request] | |
20 | + %td= link_to 'Edit', edit_project_merge_request_path(@project, merge_request) | |
21 | + %td= link_to 'Destroy', [@project, merge_request], :confirm => 'Are you sure?', :method => :delete | |
22 | + | |
23 | +%br | |
24 | + | |
25 | += link_to 'New Merge request', new_project_merge_request_path(@project) | ... | ... |
... | ... | @@ -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 | + = "'#{@merge_request.source_branch}'" | |
6 | + → | |
7 | + = "'#{@merge_request.target_branch}'" | |
8 | + .right | |
9 | + - if @merge_request.closed | |
10 | + %span.tag.high Resolved | |
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 'Resolve', 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 | + | |
44 | +- if @commits.size > 0 | |
45 | + .merge-request-commits.ui-box.width-100p | |
46 | + - @commits.each do |commit| | |
47 | + %a{ :class => "commit", :href => project_commit_path(@project, :id => commit.id) } | |
48 | + - if commit.author_email | |
49 | + = image_tag gravatar_icon(commit.author_email), :class => "left", :width => 40, :style => "padding-right:5px;" | |
50 | + - else | |
51 | + = image_tag "no_avatar.png", :class => "left", :width => 40, :style => "padding-right:5px;" | |
52 | + %span.update-title | |
53 | + = commit.id.to_s | |
54 | + %span.update-author | |
55 | + %strong= commit.author_name | |
56 | + authored | |
57 | + = time_ago_in_words(commit.created_at) | |
58 | + ago | |
59 | + .clear | ... | ... |
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/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,11 @@ |
1 | +require 'spec_helper' | |
2 | + | |
3 | +describe "MergeRequests" do | |
4 | + describe "GET /merge_requests" do | |
5 | + it "works! (now write some real specs)" do | |
6 | + # Run the generator again with the --webrat flag if you want to use webrat methods/matchers | |
7 | + get merge_requests_path | |
8 | + response.status.should be(200) | |
9 | + end | |
10 | + end | |
11 | +end | ... | ... |