Skip to content
Snippets Groups Projects
Commit 4cff66a6 authored by blackst0ne's avatar blackst0ne
Browse files

Add 'squash and rebase' feature to CE

parent 6e354cb6
No related branches found
No related tags found
No related merge requests found
Showing
with 188 additions and 31 deletions
/*
The squash-before-merge button is EE only, but it's located right in the middle
of the readyToMerge state component template.
If we didn't declare this component in CE, we'd need to maintain a separate copy
of the readyToMergeState template in EE, which is pretty big and likely to change.
Instead, in CE, we declare the component, but it's hidden and is configured to do nothing.
In EE, the configuration extends this object to add a functioning squash-before-merge
button.
*/
<script>
export default {};
import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '~/vue_merge_request_widget/event_hub';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
components: {
Icon,
},
directives: {
tooltip,
},
props: {
mr: {
type: Object,
required: true,
},
isMergeButtonDisabled: {
type: Boolean,
required: true,
},
},
data() {
return {
squashBeforeMerge: this.mr.squash,
};
},
methods: {
updateSquashModel() {
eventHub.$emit('MRWidgetUpdateSquash', this.squashBeforeMerge);
},
},
};
</script>
<template>
<div class="accept-control inline">
<label class="merge-param-checkbox">
<input
type="checkbox"
name="squash"
class="qa-squash-checkbox"
:disabled="isMergeButtonDisabled"
v-model="squashBeforeMerge"
@change="updateSquashModel"
/>
{{ __('Squash commits') }}
</label>
<a
:href="mr.squashBeforeMergeHelpPath"
data-title="About this feature"
data-placement="bottom"
target="_blank"
rel="noopener noreferrer nofollow"
data-container="body"
v-tooltip
>
<icon
name="question-o"
/>
</a>
</div>
</template>
Loading
Loading
@@ -6,11 +6,13 @@ import MergeRequest from '../../../merge_request';
import Flash from '../../../flash';
import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub';
import SquashBeforeMerge from './mr_widget_squash_before_merge.vue';
 
export default {
name: 'ReadyToMerge',
components: {
statusIcon,
'squash-before-merge': SquashBeforeMerge,
},
props: {
mr: { type: Object, required: true },
Loading
Loading
@@ -101,6 +103,12 @@ export default {
return enableSquashBeforeMerge && commitsCount > 1;
},
},
created() {
eventHub.$on('MRWidgetUpdateSquash', this.handleUpdateSquash);
},
beforeDestroy() {
eventHub.$off('MRWidgetUpdateSquash', this.handleUpdateSquash);
},
methods: {
shouldShowMergeControls() {
return this.mr.isMergeAllowed || this.shouldShowMergeWhenPipelineSucceedsText;
Loading
Loading
@@ -128,13 +136,9 @@ export default {
commit_message: this.commitMessage,
merge_when_pipeline_succeeds: this.setToMergeWhenPipelineSucceeds,
should_remove_source_branch: this.removeSourceBranch === true,
squash: this.mr.squash,
};
 
// Only truthy in EE extension of this component
if (this.setAdditionalParams) {
this.setAdditionalParams(options);
}
this.isMakingRequest = true;
this.service.merge(options)
.then(res => res.data)
Loading
Loading
@@ -154,6 +158,9 @@ export default {
new Flash('Something went wrong. Please try again.'); // eslint-disable-line
});
},
handleUpdateSquash(val) {
this.mr.squash = val;
},
initiateMergePolling() {
simplePoll((continuePolling, stopPolling) => {
this.handleMergePolling(continuePolling, stopPolling);
Loading
Loading
Loading
Loading
@@ -15,6 +15,11 @@ export default class MergeRequestStore {
const currentUser = data.current_user;
const pipelineStatus = data.pipeline ? data.pipeline.details.status : null;
 
this.squash = data.squash;
this.squashBeforeMergeHelpPath = this.squashBeforeMergeHelpPath ||
data.squash_before_merge_help_path;
this.enableSquashBeforeMerge = this.enableSquashBeforeMerge || true;
this.title = data.title;
this.targetBranch = data.target_branch;
this.sourceBranch = data.source_branch;
Loading
Loading
Loading
Loading
@@ -24,6 +24,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
:source_branch,
:source_project_id,
:state_event,
:squash,
:target_branch,
:target_project_id,
:task_num,
Loading
Loading
Loading
Loading
@@ -253,7 +253,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
 
def merge_params_attributes
[:should_remove_source_branch, :commit_message]
[:should_remove_source_branch, :commit_message, :squash]
end
 
def merge_when_pipeline_succeeds_active?
Loading
Loading
@@ -282,7 +282,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
 
return :sha_mismatch if params[:sha] != @merge_request.diff_head_sha
 
@merge_request.update(merge_error: nil)
@merge_request.update(merge_error: nil, squash: merge_params.fetch(:squash, false))
 
if params[:merge_when_pipeline_succeeds].present?
return :failed unless @merge_request.actual_head_pipeline
Loading
Loading
Loading
Loading
@@ -97,8 +97,9 @@ module MergeRequestsHelper
{
merge_when_pipeline_succeeds: true,
should_remove_source_branch: true,
sha: merge_request.diff_head_sha
}.merge(merge_params_ee(merge_request))
sha: merge_request.diff_head_sha,
squash: merge_request.squash
}
end
 
def tab_link_for(merge_request, tab, options = {}, &block)
Loading
Loading
@@ -149,8 +150,4 @@ module MergeRequestsHelper
current_user.fork_of(project)
end
end
def merge_params_ee(merge_request)
{}
end
end
Loading
Loading
@@ -1140,4 +1140,11 @@ class MergeRequest < ActiveRecord::Base
maintainer_push_possible? &&
Ability.allowed?(user, :push_code, source_project)
end
def squash_in_progress?
# The source project can be deleted
return false unless source_project
source_project.repository.squash_in_progress?(id)
end
end
Loading
Loading
@@ -957,6 +957,14 @@ class Repository
remote_branch: merge_request.target_branch)
end
 
def squash(user, merge_request)
raw.squash(user, merge_request.id, branch: merge_request.target_branch,
start_sha: merge_request.diff_start_sha,
end_sha: merge_request.diff_head_sha,
author: merge_request.author,
message: merge_request.title)
end
private
 
# TODO Generice finder, later split this on finders by Ref or Oid
Loading
Loading
Loading
Loading
@@ -10,6 +10,7 @@ class MergeRequestWidgetEntity < IssuableEntity
expose :merge_when_pipeline_succeeds
expose :source_branch
expose :source_project_id
expose :squash
expose :target_branch
expose :target_project_id
expose :allow_maintainer_to_push
Loading
Loading
Loading
Loading
@@ -34,6 +34,19 @@ module MergeRequests
handle_merge_error(log_message: e.message, save_message_on_model: true)
end
 
def source
return merge_request.diff_head_sha unless merge_request.squash
squash_result = ::MergeRequests::SquashService.new(project, current_user, params).execute(merge_request)
case squash_result[:status]
when :success
squash_result[:squash_sha]
when :error
raise ::MergeRequests::MergeService::MergeError, squash_result[:message]
end
end
private
 
def error_check!
Loading
Loading
@@ -116,9 +129,5 @@ module MergeRequests
def merge_request_info
merge_request.to_reference(full: true)
end
def source
@source ||= @merge_request.diff_head_sha
end
end
end
module MergeRequests
class SquashService < MergeRequests::WorkingCopyBaseService
def execute(merge_request)
@merge_request = merge_request
@repository = target_project.repository
squash || error('Failed to squash. Should be done manually.')
end
def squash
if merge_request.commits_count < 2
return success(squash_sha: merge_request.diff_head_sha)
end
if merge_request.squash_in_progress?
return error('Squash task canceled: another squash is already in progress.')
end
squash_sha = repository.squash(current_user, merge_request)
success(squash_sha: squash_sha)
rescue => e
log_error("Failed to squash merge request #{merge_request.to_reference(full: true)}:")
log_error(e.message)
false
end
end
end
Loading
Loading
@@ -20,6 +20,8 @@
window.gl = window.gl || {};
window.gl.mrWidgetData = #{serialize_issuable(@merge_request, serializer: 'widget')}
 
window.gl.mrWidgetData.squash_before_merge_help_path = '#{help_page_path("user/project/merge_requests/squash_and_merge")}';
#js-vue-mr-widget.mr-widget
 
.content-block.content-block-small.emoji-list-container.js-noteable-awards
Loading
Loading
Loading
Loading
@@ -15,3 +15,12 @@
= hidden_field_tag 'merge_request[force_remove_source_branch]', '0', id: nil
= check_box_tag 'merge_request[force_remove_source_branch]', '1', issuable.force_remove_source_branch?
Remove source branch when merge request is accepted.
.form-group
.col-sm-10.col-sm-offset-2
.checkbox
= label_tag 'merge_request[squash]' do
= hidden_field_tag 'merge_request[squash]', '0', id: nil
= check_box_tag 'merge_request[squash]', '1', issuable.squash
Squash commits when merge request is accepted.
= link_to 'About this feature', help_page_path('user/project/merge_requests/squash_and_merge')
---
title: Add `Squash and merge` to GitLab Core (CE)
merge_request: 18956
author: "@blackst0ne"
type: added
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddSquashToMergeRequests < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
DOWNTIME = false
def up
unless column_exists?(:merge_requests, :squash)
add_column_with_default :merge_requests, :squash, :boolean, default: false, allow_null: false
end
end
def down
remove_column :merge_requests, :squash if column_exists?(:merge_requests, :squash)
end
end
Loading
Loading
@@ -1217,6 +1217,7 @@ ActiveRecord::Schema.define(version: 20180521171529) do
t.integer "latest_merge_request_diff_id"
t.string "rebase_commit_sha"
t.boolean "allow_maintainer_to_push"
t.boolean "squash", default: false, null: false
end
 
add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree
Loading
Loading
Loading
Loading
@@ -107,6 +107,7 @@ Parameters:
"changes_count": "1",
"should_remove_source_branch": true,
"force_remove_source_branch": false,
"squash": false,
"web_url": "http://example.com/example/example/merge_requests/1",
"time_stats": {
"time_estimate": 0,
Loading
Loading
@@ -226,6 +227,7 @@ Parameters:
"changes_count": "1",
"should_remove_source_branch": true,
"force_remove_source_branch": false,
"squash": false,
"web_url": "http://example.com/example/example/merge_requests/1",
"discussion_locked": false,
"time_stats": {
Loading
Loading
@@ -305,6 +307,7 @@ Parameters:
"changes_count": "1",
"should_remove_source_branch": true,
"force_remove_source_branch": false,
"squash": false,
"web_url": "http://example.com/example/example/merge_requests/1",
"discussion_locked": false,
"time_stats": {
Loading
Loading
@@ -541,7 +544,8 @@ POST /projects/:id/merge_requests
| `labels` | string | no | Labels for MR as a comma-separated list |
| `milestone_id` | integer | no | The global ID of a milestone |
| `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging |
| `allow_maintainer_to_push` | boolean | no | Whether or not a maintainer of the target project can push to the source branch |
| `allow_maintainer_to_push` | boolean | no | Whether or not a maintainer of the target project can push to the source branch |
| `squash` | boolean | no | Squash commits into a single commit when merging |
 
```json
{
Loading
Loading
@@ -595,6 +599,7 @@ POST /projects/:id/merge_requests
"changes_count": "1",
"should_remove_source_branch": true,
"force_remove_source_branch": false,
"squash": false,
"web_url": "http://example.com/example/example/merge_requests/1",
"discussion_locked": false,
"allow_maintainer_to_push": false,
Loading
Loading
@@ -627,6 +632,7 @@ PUT /projects/:id/merge_requests/:merge_request_iid
| `description` | string | no | Description of MR |
| `state_event` | string | no | New state (close/reopen) |
| `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging |
| `squash` | boolean | no | Squash commits into a single commit when merging |
| `discussion_locked` | boolean | no | Flag indicating if the merge request's discussion is locked. If the discussion is locked only project members can add, edit or resolve comments. |
| `allow_maintainer_to_push` | boolean | no | Whether or not a maintainer of the target project can push to the source branch |
 
Loading
Loading
@@ -683,6 +689,7 @@ Must include at least one non-required attribute from above.
"changes_count": "1",
"should_remove_source_branch": true,
"force_remove_source_branch": false,
"squash": false,
"web_url": "http://example.com/example/example/merge_requests/1",
"discussion_locked": false,
"allow_maintainer_to_push": false,
Loading
Loading
@@ -790,6 +797,7 @@ Parameters:
"changes_count": "1",
"should_remove_source_branch": true,
"force_remove_source_branch": false,
"squash": false,
"web_url": "http://example.com/example/example/merge_requests/1",
"discussion_locked": false,
"time_stats": {
Loading
Loading
@@ -868,6 +876,7 @@ Parameters:
"changes_count": "1",
"should_remove_source_branch": true,
"force_remove_source_branch": false,
"squash": false,
"web_url": "http://example.com/example/example/merge_requests/1",
"discussion_locked": false,
"time_stats": {
Loading
Loading
@@ -1200,6 +1209,7 @@ Example response:
"changes_count": "1",
"should_remove_source_branch": true,
"force_remove_source_branch": false,
"squash": false,
"web_url": "http://example.com/example/example/merge_requests/1"
},
"target_url": "https://gitlab.example.com/gitlab-org/gitlab-ci/merge_requests/7",
Loading
Loading
doc/user/project/merge_requests/img/squash_edit_form.png

4.13 KiB

doc/user/project/merge_requests/img/squash_mr_commits.png

83.6 KiB

doc/user/project/merge_requests/img/squash_mr_widget.png

6.34 KiB

0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment