limit_ee_conflicts.md 13.7 KB
Newer Older
1 2 3 4 5 6 7 8 9 10
# Limit conflicts with EE when developing on CE

This guide contains best-practices for avoiding conflicts between CE and EE.

## Context

Usually, GitLab Community Edition is merged into the Enterprise Edition once a
week. During these merges, it's very common to get conflicts when some changes
in CE do not apply cleanly to EE.

11
There are a few things that can help you as a developer to:
12

13 14 15
- know when your merge request to CE will conflict when merged to EE
- avoid such conflicts in the first place
- ease future conflict resolutions if conflict is inevitable
16

17
## Check the `rake ee_compat_check` in your merge requests
18

19 20
For each commit (except on `master`), the `rake ee_compat_check` CI job tries to
detect if the current branch's changes will conflict during the CE->EE merge.
21

22 23
The job reports what files are conflicting and how to setup a merge request
against EE. Here is roughly how it works:
24

25 26 27 28 29 30 31
1. Generates the diff between your branch and current CE `master`
1. Tries to apply it to current EE `master`
1. If it applies cleanly, the job succeeds, otherwise...
1. Detects a branch with the `-ee` suffix in EE
1. If it exists, generate the diff between this branch and current EE `master`
1. Tries to apply it to current EE `master`
1. If it applies cleanly, the job succeeds
32

33 34 35
In the case where the job fails, it means you should create a `<ce_branch>-ee`
branch, push it to EE and open a merge request against EE `master`. At this
point if you retry the failing job in your CE merge request, it should now pass.
36

37
Notes:
38

39 40 41 42 43 44 45
- This task is not a silver-bullet, its current goal is to bring awareness to
  developers that their work needs to be ported to EE.
- Community contributors shouldn't submit merge requests against EE, but
  reviewers should take actions by either creating such EE merge request or
  asking a GitLab developer to do it once the merge request is merged.
- If you branch is more than 500 commits behind `master`, the job will fail and
  you should rebase your branch upon latest `master`.
46

47
## Possible type of conflicts
48 49 50

### Controllers

51
#### List or arrays are augmented in EE
52

R
Rémy Coutable 已提交
53 54
In controllers, the most common type of conflict is with `before_action` that
has a list of actions in CE but EE adds some actions to that list.
55

R
Rémy Coutable 已提交
56
The same problem often occurs for `params.require` / `params.permit` calls.
57

58
##### Mitigations
59 60 61 62 63 64 65 66

Separate CE and EE actions/keywords. For instance for `params.require` in
`ProjectsController`:

```ruby
def project_params
  params.require(:project).permit(project_params_ce)
  # On EE, this is always:
R
Rémy Coutable 已提交
67
  # params.require(:project).permit(project_params_ce << project_params_ee)
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
end

# Always returns an array of symbols, created however best fits the use case.
# It _should_ be sorted alphabetically.
def project_params_ce
  %i[
    description
    name
    path
  ]
end

# (On EE)
def project_params_ee
  %i[
    approvals_before_merge
    approver_group_ids
    approver_ids
    ...
  ]
end
```

91 92 93 94 95 96 97 98 99 100 101 102 103 104
#### Additional condition(s) in EE

For instance for LDAP:

```diff
    def destroy
      @key = current_user.keys.find(params[:id])
 -    @key.destroy
 +    @key.destroy unless @key.is_a? LDAPKey

      respond_to do |format|
```

Or for Geo:
105

106 107 108 109 110 111 112 113 114 115
```diff
def after_sign_out_path_for(resource)
-    current_application_settings.after_sign_out_path.presence || new_user_session_path
+    if Gitlab::Geo.secondary?
+      Gitlab::Geo.primary_node.oauth_logout_url(@geo_logout_state)
+    else
+      current_application_settings.after_sign_out_path.presence || new_user_session_path
+    end
end
```
116

117
Or even for audit log:
118

119 120 121 122 123 124
```diff
def approve_access_request
-    Members::ApproveAccessRequestService.new(membershipable, current_user, params).execute
+    member = Members::ApproveAccessRequestService.new(membershipable, current_user, params).execute
+
+    log_audit_event(member, action: :create)
125

126 127 128 129 130 131 132 133 134 135 136 137
  redirect_to polymorphic_url([membershipable, :members])
end
```

### Views

#### Additional view code in EE

A block of code added in CE conflicts because there is already another block
at the same place in EE

##### Mitigations
138 139

Blocks of code that are EE-specific should be moved to partials as much as
140
possible to avoid conflicts with big chunks of HAML code that that are not fun
R
Rémy Coutable 已提交
141
to resolve when you add the indentation to the equation.
142

R
Rémy Coutable 已提交
143
For instance this kind of thing:
144 145

```haml
146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
.form-group.detail-page-description
  = form.label :description, 'Description', class: 'control-label'
  .col-sm-10
    = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
      = render 'projects/zen', f: form, attr: :description,
                               classes: 'note-textarea',
                               placeholder: "Write a comment or drag your files here...",
                               supports_slash_commands: !issuable.persisted?
      = render 'projects/notes/hints', supports_slash_commands: !issuable.persisted?
      .clearfix
      .error-alert
- if issuable.is_a?(Issue)
  .form-group
    .col-sm-offset-2.col-sm-10
      .checkbox
        = form.label :confidential do
          = form.check_box :confidential
          This issue is confidential and should only be visible to team members with at least Reporter access.
164 165 166 167 168 169
- if can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project)
  - has_due_date = issuable.has_attribute?(:due_date)
  %hr
  .row
    %div{ class: (has_due_date ? "col-lg-6" : "col-sm-12") }
      .form-group.issue-assignee
170
        = form.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}"
171 172 173
        .col-sm-10{ class: ("col-lg-8" if has_due_date) }
          .issuable-form-select-holder
            - if issuable.assignee_id
174
              = form.hidden_field :assignee_id
175 176 177
            = dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Select assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit",
              placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee"} })
      .form-group.issue-milestone
178
        = form.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}"
179 180 181 182 183
        .col-sm-10{ class: ("col-lg-8" if has_due_date) }
          .issuable-form-select-holder
            = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone"
      .form-group
        - has_labels = @labels && @labels.any?
184 185
        = form.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}"
        = form.hidden_field :label_ids, multiple: true, value: ''
186 187
        .col-sm-10{ class: "#{"col-lg-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" }
          .issuable-form-select-holder
188
            = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false }, dropdown_title: "Select label"
189
      - if issuable.respond_to?(:weight)
190 191 192
        - weight_options = Issue.weight_options
        - weight_options.delete(Issue::WEIGHT_ALL)
        - weight_options.delete(Issue::WEIGHT_ANY)
193
        .form-group
194
          = form.label :label_ids, class: "control-label #{"col-lg-4" if has_due_date}" do
195 196
            Weight
          .col-sm-10{ class: ("col-lg-8" if has_due_date) }
197 198 199 200 201 202 203 204 205 206
            .issuable-form-select-holder
              - if issuable.weight
                = form.hidden_field :weight
              = dropdown_tag(issuable.weight || "Weight", options: { title: "Select weight", toggle_class: 'js-weight-select js-issuable-form-weight', dropdown_class: "dropdown-menu-selectable dropdown-menu-weight",
                placeholder: "Search weight", data: { field_name: "#{issuable.class.model_name.param_key}[weight]" , default_label: "Weight" } }) do
                %ul
                  - weight_options.each do |weight|
                    %li
                      %a{href: "#", data: { id: weight, none: weight === Issue::WEIGHT_NONE }, class: ("is-active" if issuable.weight == weight)}
                        = weight
207 208 209
    - if has_due_date
      .col-lg-6
        .form-group
210
          = form.label :due_date, "Due date", class: "control-label"
211 212
          .col-sm-10
            .issuable-form-select-holder
213
              = form.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: "Select due date"
214 215 216 217 218
```

could be simplified by using partials:

```haml
219 220 221 222 223 224 225 226 227 228 229
= render 'shared/issuable/form/description', issuable: issuable, form: form

- if issuable.respond_to?(:confidential)
  .form-group
    .col-sm-offset-2.col-sm-10
      .checkbox
        = form.label :confidential do
          = form.check_box :confidential
          This issue is confidential and should only be visible to team members with at least Reporter access.

= render 'shared/issuable/form/metadata', issuable: issuable, form: form
230 231
```

232
and then the `app/views/shared/issuable/form/_metadata.html.haml` could be as follows:
233 234

```haml
235 236
- issuable = local_assigns.fetch(:issuable)

237 238 239
- return unless can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project)

- has_due_date = issuable.has_attribute?(:due_date)
240 241 242
- has_labels = @labels && @labels.any?
- form = local_assigns.fetch(:form)

243 244 245 246
%hr
.row
  %div{ class: (has_due_date ? "col-lg-6" : "col-sm-12") }
    .form-group.issue-assignee
247
      = form.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}"
248 249 250
      .col-sm-10{ class: ("col-lg-8" if has_due_date) }
        .issuable-form-select-holder
          - if issuable.assignee_id
251
            = form.hidden_field :assignee_id
252
          = dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Select assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit",
253
            placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: issuable.project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee"} })
254
    .form-group.issue-milestone
255
      = form.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}"
256 257 258 259 260
      .col-sm-10{ class: ("col-lg-8" if has_due_date) }
        .issuable-form-select-holder
          = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone"
    .form-group
      - has_labels = @labels && @labels.any?
261 262
      = form.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}"
      = form.hidden_field :label_ids, multiple: true, value: ''
263 264
      .col-sm-10{ class: "#{"col-lg-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" }
        .issuable-form-select-holder
265
          = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false }, dropdown_title: "Select label"
266

267
    = render "shared/issuable/form/weight", issuable: issuable, form: form
268 269 270 271

  - if has_due_date
    .col-lg-6
      .form-group
272
        = form.label :due_date, "Due date", class: "control-label"
273 274
        .col-sm-10
          .issuable-form-select-holder
275
            = form.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: "Select due date"
276 277
```

278
and then the `app/views/shared/issuable/form/_weight.html.haml` could be as follows:
279 280

```haml
281 282
- issuable = local_assigns.fetch(:issuable)

283 284 285
- return unless issuable.respond_to?(:weight)

- has_due_date = issuable.has_attribute?(:due_date)
286
- form = local_assigns.fetch(:form)
287 288

.form-group
289
  = form.label :label_ids, class: "control-label #{"col-lg-4" if has_due_date}" do
290 291
    Weight
  .col-sm-10{ class: ("col-lg-8" if has_due_date) }
292 293 294 295 296 297 298 299 300 301
    .issuable-form-select-holder
      - if issuable.weight
        = form.hidden_field :weight

      = weight_dropdown_tag(issuable, toggle_class: 'js-issuable-form-weight') do
        %ul
          - Issue.weight_options.each do |weight|
            %li
              %a{ href: '#', data: { id: weight, none: weight === Issue::WEIGHT_NONE }, class: ("is-active" if issuable.weight == weight) }
                = weight
302 303 304 305
```

Note:

R
Rémy Coutable 已提交
306
- The safeguards at the top allow to get rid of an unneccessary indentation level
307 308 309 310 311
- Here we only moved the 'Weight' code to a partial since this is the only
  EE-specific code in that view, so it's the most likely to conflict, but you
  are encouraged to use partials even for code that's in CE to logically split
  big views into several smaller files.

312 313 314 315 316 317 318 319 320 321 322
#### Indentation issue

Sometimes a code block is indented more or less in EE because there's an
additional condition.

##### Mitigations

Blocks of code that are EE-specific should be moved to partials as much as
possible to avoid conflicts with big chunks of HAML code that that are not fun
to resolve when you add the indentation in the equation.

323 324 325
---

[Return to Development documentation](README.md)