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,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, |
@@ -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 | +} |
@@ -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 < ApplicationController | @@ -42,6 +42,8 @@ class NotesController < 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/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 | [ |
@@ -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 "grit" | @@ -3,6 +3,7 @@ require "grit" | ||
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 |
@@ -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 | + |
@@ -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 | + |
@@ -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 | + |
@@ -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 | + → | ||
13 | + %span.tag.commit= merge_request.target_branch | ||
14 | + |
@@ -0,0 +1 @@ | @@ -0,0 +1 @@ | ||
1 | += render 'form' |
@@ -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" |
@@ -0,0 +1 @@ | @@ -0,0 +1 @@ | ||
1 | += render 'form' |
@@ -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 | + | ||
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
@@ -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 |
@@ -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 |
db/schema.rb
@@ -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 => 20111124115339) do | @@ -21,6 +21,8 @@ ActiveRecord::Schema.define(:version => 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 => 20111124115339) do | @@ -45,6 +47,18 @@ ActiveRecord::Schema.define(:version => 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 |
@@ -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 "Projects" do | @@ -122,5 +122,14 @@ describe "Projects" 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 |