Commit 537f87a1 authored by Stan Hu's avatar Stan Hu

Merge branch 'master' into sh-support-bitbucket-server-import

parents f94b5225 d22db4f4
...@@ -447,9 +447,8 @@ danger-review: ...@@ -447,9 +447,8 @@ danger-review:
- retry gem install danger --no-ri --no-rdoc - retry gem install danger --no-ri --no-rdoc
cache: {} cache: {}
only: only:
refs: variables:
- branches@gitlab-org/gitlab-ce - $DANGER_GITLAB_API_TOKEN
- branches@gitlab-org/gitlab-ee
except: except:
refs: refs:
- master - master
......
...@@ -2,6 +2,18 @@ ...@@ -2,6 +2,18 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 11.1.1 (2018-07-23)
### Fixed (2 changes)
- Add missing Gitaly branch_update nil checks. !20711
- Fix filename for accelerated uploads.
### Added (1 change)
- Add uploader support to Import/Export uploads. !20484
## 11.1.0 (2018-07-22) ## 11.1.0 (2018-07-22)
### Security (6 changes) ### Security (6 changes)
......
...@@ -133,7 +133,7 @@ Most issues will have labels for at least one of the following: ...@@ -133,7 +133,7 @@ Most issues will have labels for at least one of the following:
- Type: ~"feature proposal", ~bug, ~customer, etc. - Type: ~"feature proposal", ~bug, ~customer, etc.
- Subject: ~wiki, ~"container registry", ~ldap, ~api, ~frontend, etc. - Subject: ~wiki, ~"container registry", ~ldap, ~api, ~frontend, etc.
- Team: ~"CI/CD", ~Plan, ~Quality, ~Platform, etc. - Team: ~"CI/CD", ~Plan, ~Manage, ~Quality, etc.
- Release Scoping: ~Deliverable, ~Stretch, ~"Next Patch Release" - Release Scoping: ~Deliverable, ~Stretch, ~"Next Patch Release"
- Priority: ~P1, ~P2, ~P3, ~P4 - Priority: ~P1, ~P2, ~P3, ~P4
- Severity: ~S1, ~S2, ~S3, ~S4 - Severity: ~S1, ~S2, ~S3, ~S4
...@@ -192,9 +192,9 @@ The current team labels are: ...@@ -192,9 +192,9 @@ The current team labels are:
- ~Documentation - ~Documentation
- ~Geo - ~Geo
- ~Gitaly - ~Gitaly
- ~Manage
- ~Monitoring - ~Monitoring
- ~Plan - ~Plan
- ~Platform
- ~Quality - ~Quality
- ~Release - ~Release
- ~"Security Products" - ~"Security Products"
...@@ -376,8 +376,14 @@ on those issues. Please select someone with relevant experience from the ...@@ -376,8 +376,14 @@ on those issues. Please select someone with relevant experience from the
[GitLab team][team]. If there is nobody mentioned with that expertise look in [GitLab team][team]. If there is nobody mentioned with that expertise look in
the commit history for the affected files to find someone. the commit history for the affected files to find someone.
We also use [GitLab Triage] to automate some triaging policies. This is
currently setup as a [scheduled pipeline] running on the [`gl-triage`] branch.
[described in our handbook]: https://about.gitlab.com/handbook/engineering/issue-triage/ [described in our handbook]: https://about.gitlab.com/handbook/engineering/issue-triage/
[issue bash events]: https://gitlab.com/gitlab-org/gitlab-ce/issues/17815 [issue bash events]: https://gitlab.com/gitlab-org/gitlab-ce/issues/17815
[GitLab Triage]: https://gitlab.com/gitlab-org/gitlab-triage
[scheduled pipeline]: https://gitlab.com/gitlab-org/gitlab-ce/pipeline_schedules/3732/edit
[`gl-triage`]: https://gitlab.com/gitlab-org/gitlab-ce/tree/gl-triage
### Feature proposals ### Feature proposals
......
...@@ -4,3 +4,4 @@ danger.import_dangerfile(path: 'danger/changelog') ...@@ -4,3 +4,4 @@ danger.import_dangerfile(path: 'danger/changelog')
danger.import_dangerfile(path: 'danger/specs') danger.import_dangerfile(path: 'danger/specs')
danger.import_dangerfile(path: 'danger/gemfile') danger.import_dangerfile(path: 'danger/gemfile')
danger.import_dangerfile(path: 'danger/database') danger.import_dangerfile(path: 'danger/database')
danger.import_dangerfile(path: 'danger/frozen_string')
...@@ -220,6 +220,9 @@ gem 'gemnasium-gitlab-service', '~> 0.2' ...@@ -220,6 +220,9 @@ gem 'gemnasium-gitlab-service', '~> 0.2'
# Slack integration # Slack integration
gem 'slack-notifier', '~> 1.5.1' gem 'slack-notifier', '~> 1.5.1'
# Hangouts Chat integration
gem 'hangouts-chat', '~> 0.0.5'
# Asana integration # Asana integration
gem 'asana', '~> 0.6.0' gem 'asana', '~> 0.6.0'
...@@ -230,7 +233,7 @@ gem 'ruby-fogbugz', '~> 0.2.1' ...@@ -230,7 +233,7 @@ gem 'ruby-fogbugz', '~> 0.2.1'
gem 'kubeclient', '~> 3.1.0' gem 'kubeclient', '~> 3.1.0'
# Sanitize user input # Sanitize user input
gem 'sanitize', '~> 4.6.5' gem 'sanitize', '~> 4.6'
gem 'babosa', '~> 1.0.2' gem 'babosa', '~> 1.0.2'
# Sanitizes SVG input # Sanitizes SVG input
...@@ -419,7 +422,7 @@ group :ed25519 do ...@@ -419,7 +422,7 @@ group :ed25519 do
end end
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly-proto', '~> 0.106.0', require: 'gitaly' gem 'gitaly-proto', '~> 0.109.0', require: 'gitaly'
gem 'grpc', '~> 1.11.0' gem 'grpc', '~> 1.11.0'
# Locked until https://github.com/google/protobuf/issues/4210 is closed # Locked until https://github.com/google/protobuf/issues/4210 is closed
......
...@@ -284,7 +284,7 @@ GEM ...@@ -284,7 +284,7 @@ GEM
gettext_i18n_rails (>= 0.7.1) gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gitaly-proto (0.106.0) gitaly-proto (0.109.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.10) grpc (~> 1.10)
github-linguist (5.3.3) github-linguist (5.3.3)
...@@ -387,6 +387,7 @@ GEM ...@@ -387,6 +387,7 @@ GEM
temple (>= 0.8.0) temple (>= 0.8.0)
thor thor
tilt tilt
hangouts-chat (0.0.5)
hashdiff (0.3.4) hashdiff (0.3.4)
hashie (3.5.7) hashie (3.5.7)
hashie-forbidden_attributes (0.1.1) hashie-forbidden_attributes (0.1.1)
...@@ -396,7 +397,7 @@ GEM ...@@ -396,7 +397,7 @@ GEM
hipchat (1.5.2) hipchat (1.5.2)
httparty httparty
mimemagic mimemagic
html-pipeline (2.8.3) html-pipeline (2.8.4)
activesupport (>= 2) activesupport (>= 2)
nokogiri (>= 1.4) nokogiri (>= 1.4)
html2text (0.2.0) html2text (0.2.0)
...@@ -513,7 +514,7 @@ GEM ...@@ -513,7 +514,7 @@ GEM
net-ldap (0.16.0) net-ldap (0.16.0)
net-ssh (5.0.1) net-ssh (5.0.1)
netrc (0.11.0) netrc (0.11.0)
nokogiri (1.8.3) nokogiri (1.8.4)
mini_portile2 (~> 2.3.0) mini_portile2 (~> 2.3.0)
nokogumbo (1.5.0) nokogumbo (1.5.0)
nokogiri nokogiri
...@@ -807,7 +808,7 @@ GEM ...@@ -807,7 +808,7 @@ GEM
et-orbi (~> 1.0) et-orbi (~> 1.0)
rugged (0.27.2) rugged (0.27.2)
safe_yaml (1.0.4) safe_yaml (1.0.4)
sanitize (4.6.5) sanitize (4.6.6)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.4.4) nokogiri (>= 1.4.4)
nokogumbo (~> 1.4) nokogumbo (~> 1.4)
...@@ -1041,7 +1042,7 @@ DEPENDENCIES ...@@ -1041,7 +1042,7 @@ DEPENDENCIES
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3) gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 0.106.0) gitaly-proto (~> 0.109.0)
github-linguist (~> 5.3.3) github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2) gitlab-gollum-lib (~> 4.2)
...@@ -1062,6 +1063,7 @@ DEPENDENCIES ...@@ -1062,6 +1063,7 @@ DEPENDENCIES
grpc (~> 1.11.0) grpc (~> 1.11.0)
haml_lint (~> 0.26.0) haml_lint (~> 0.26.0)
hamlit (~> 2.8.8) hamlit (~> 2.8.8)
hangouts-chat (~> 0.0.5)
hashie-forbidden_attributes hashie-forbidden_attributes
health_check (~> 2.6.0) health_check (~> 2.6.0)
hipchat (~> 1.5.0) hipchat (~> 1.5.0)
...@@ -1155,7 +1157,7 @@ DEPENDENCIES ...@@ -1155,7 +1157,7 @@ DEPENDENCIES
ruby_parser (~> 3.8) ruby_parser (~> 3.8)
rufus-scheduler (~> 3.4) rufus-scheduler (~> 3.4)
rugged (~> 0.27) rugged (~> 0.27)
sanitize (~> 4.6.5) sanitize (~> 4.6)
sass-rails (~> 5.0.6) sass-rails (~> 5.0.6)
scss_lint (~> 0.56.0) scss_lint (~> 0.56.0)
seed-fu (~> 2.3.7) seed-fu (~> 2.3.7)
......
...@@ -287,7 +287,7 @@ GEM ...@@ -287,7 +287,7 @@ GEM
gettext_i18n_rails (>= 0.7.1) gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gitaly-proto (0.106.0) gitaly-proto (0.109.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.10) grpc (~> 1.10)
github-linguist (5.3.3) github-linguist (5.3.3)
...@@ -390,6 +390,7 @@ GEM ...@@ -390,6 +390,7 @@ GEM
temple (>= 0.8.0) temple (>= 0.8.0)
thor thor
tilt tilt
hangouts-chat (0.0.5)
hashdiff (0.3.4) hashdiff (0.3.4)
hashie (3.5.7) hashie (3.5.7)
hashie-forbidden_attributes (0.1.1) hashie-forbidden_attributes (0.1.1)
...@@ -1051,7 +1052,7 @@ DEPENDENCIES ...@@ -1051,7 +1052,7 @@ DEPENDENCIES
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3) gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 0.106.0) gitaly-proto (~> 0.109.0)
github-linguist (~> 5.3.3) github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2) gitlab-gollum-lib (~> 4.2)
...@@ -1072,6 +1073,7 @@ DEPENDENCIES ...@@ -1072,6 +1073,7 @@ DEPENDENCIES
grpc (~> 1.11.0) grpc (~> 1.11.0)
haml_lint (~> 0.26.0) haml_lint (~> 0.26.0)
hamlit (~> 2.8.8) hamlit (~> 2.8.8)
hangouts-chat (~> 0.0.5)
hashie-forbidden_attributes hashie-forbidden_attributes
health_check (~> 2.6.0) health_check (~> 2.6.0)
hipchat (~> 1.5.0) hipchat (~> 1.5.0)
...@@ -1166,7 +1168,7 @@ DEPENDENCIES ...@@ -1166,7 +1168,7 @@ DEPENDENCIES
ruby_parser (~> 3.8) ruby_parser (~> 3.8)
rufus-scheduler (~> 3.4) rufus-scheduler (~> 3.4)
rugged (~> 0.27) rugged (~> 0.27)
sanitize (~> 4.6.5) sanitize (~> 4.6)
sass-rails (~> 5.0.6) sass-rails (~> 5.0.6)
scss_lint (~> 0.56.0) scss_lint (~> 0.56.0)
seed-fu (~> 2.3.7) seed-fu (~> 2.3.7)
......
...@@ -71,6 +71,11 @@ export default { ...@@ -71,6 +71,11 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
discussions: {
type: Array,
required: false,
default: () => [],
},
}, },
computed: { computed: {
...mapState({ ...mapState({
...@@ -78,7 +83,6 @@ export default { ...@@ -78,7 +83,6 @@ export default {
diffFiles: state => state.diffs.diffFiles, diffFiles: state => state.diffs.diffFiles,
}), }),
...mapGetters(['isLoggedIn']), ...mapGetters(['isLoggedIn']),
...mapGetters('diffs', ['discussionsByLineCode']),
lineHref() { lineHref() {
return this.lineCode ? `#${this.lineCode}` : '#'; return this.lineCode ? `#${this.lineCode}` : '#';
}, },
...@@ -88,24 +92,19 @@ export default { ...@@ -88,24 +92,19 @@ export default {
this.showCommentButton && this.showCommentButton &&
!this.isMatchLine && !this.isMatchLine &&
!this.isContextLine && !this.isContextLine &&
!this.hasDiscussions && !this.isMetaLine &&
!this.isMetaLine !this.hasDiscussions
); );
}, },
discussions() {
return this.discussionsByLineCode[this.lineCode] || [];
},
hasDiscussions() { hasDiscussions() {
return this.discussions.length > 0; return this.discussions.length > 0;
}, },
shouldShowAvatarsOnGutter() { shouldShowAvatarsOnGutter() {
let render = this.hasDiscussions && this.showCommentButton;
if (!this.lineType && this.linePosition === LINE_POSITION_RIGHT) { if (!this.lineType && this.linePosition === LINE_POSITION_RIGHT) {
render = false; return false;
} }
return render; return this.hasDiscussions && this.showCommentButton;
}, },
}, },
methods: { methods: {
......
...@@ -67,6 +67,11 @@ export default { ...@@ -67,6 +67,11 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
discussions: {
type: Array,
required: false,
default: () => [],
},
}, },
computed: { computed: {
...mapGetters(['isLoggedIn']), ...mapGetters(['isLoggedIn']),
...@@ -136,6 +141,7 @@ export default { ...@@ -136,6 +141,7 @@ export default {
:is-match-line="isMatchLine" :is-match-line="isMatchLine"
:is-context-line="isContentLine" :is-context-line="isContentLine"
:is-meta-line="isMetaLine" :is-meta-line="isMetaLine"
:discussions="discussions"
/> />
</td> </td>
</template> </template>
<script> <script>
import { mapState, mapGetters } from 'vuex'; import { mapState } from 'vuex';
import diffDiscussions from './diff_discussions.vue'; import diffDiscussions from './diff_discussions.vue';
import diffLineNoteForm from './diff_line_note_form.vue'; import diffLineNoteForm from './diff_line_note_form.vue';
...@@ -21,15 +21,16 @@ export default { ...@@ -21,15 +21,16 @@ export default {
type: Number, type: Number,
required: true, required: true,
}, },
discussions: {
type: Array,
required: false,
default: () => [],
},
}, },
computed: { computed: {
...mapState({ ...mapState({
diffLineCommentForms: state => state.diffs.diffLineCommentForms, diffLineCommentForms: state => state.diffs.diffLineCommentForms,
}), }),
...mapGetters('diffs', ['discussionsByLineCode']),
discussions() {
return this.discussionsByLineCode[this.line.lineCode] || [];
},
className() { className() {
return this.discussions.length ? '' : 'js-temp-notes-holder'; return this.discussions.length ? '' : 'js-temp-notes-holder';
}, },
......
...@@ -33,6 +33,11 @@ export default { ...@@ -33,6 +33,11 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
discussions: {
type: Array,
required: false,
default: () => [],
},
}, },
data() { data() {
return { return {
...@@ -89,6 +94,7 @@ export default { ...@@ -89,6 +94,7 @@ export default {
:is-bottom="isBottom" :is-bottom="isBottom"
:is-hover="isHover" :is-hover="isHover"
:show-comment-button="true" :show-comment-button="true"
:discussions="discussions"
class="diff-line-num old_line" class="diff-line-num old_line"
/> />
<diff-table-cell <diff-table-cell
...@@ -98,6 +104,7 @@ export default { ...@@ -98,6 +104,7 @@ export default {
:line-type="newLineType" :line-type="newLineType"
:is-bottom="isBottom" :is-bottom="isBottom"
:is-hover="isHover" :is-hover="isHover"
:discussions="discussions"
class="diff-line-num new_line" class="diff-line-num new_line"
/> />
<td <td
......
...@@ -20,7 +20,11 @@ export default { ...@@ -20,7 +20,11 @@ export default {
}, },
}, },
computed: { computed: {
...mapGetters('diffs', ['commitId', 'discussionsByLineCode']), ...mapGetters('diffs', [
'commitId',
'shouldRenderInlineCommentRow',
'singleDiscussionByLineCode',
]),
...mapState({ ...mapState({
diffLineCommentForms: state => state.diffs.diffLineCommentForms, diffLineCommentForms: state => state.diffs.diffLineCommentForms,
}), }),
...@@ -34,18 +38,7 @@ export default { ...@@ -34,18 +38,7 @@ export default {
return window.gon.user_color_scheme; return window.gon.user_color_scheme;
}, },
}, },
methods: { methods: {},
shouldRenderCommentRow(line) {
if (this.diffLineCommentForms[line.lineCode]) return true;
const lineDiscussions = this.discussionsByLineCode[line.lineCode];
if (lineDiscussions === undefined) {
return false;
}
return lineDiscussions.every(discussion => discussion.expanded);
},
},
}; };
</script> </script>
...@@ -64,13 +57,15 @@ export default { ...@@ -64,13 +57,15 @@ export default {
:line="line" :line="line"
:is-bottom="index + 1 === diffLinesLength" :is-bottom="index + 1 === diffLinesLength"
:key="line.lineCode" :key="line.lineCode"
:discussions="singleDiscussionByLineCode(line.lineCode)"
/> />
<inline-diff-comment-row <inline-diff-comment-row
v-if="shouldRenderCommentRow(line)" v-if="shouldRenderInlineCommentRow(line)"
:diff-file-hash="diffFile.fileHash" :diff-file-hash="diffFile.fileHash"
:line="line" :line="line"
:line-index="index" :line-index="index"
:key="index" :key="index"
:discussions="singleDiscussionByLineCode(line.lineCode)"
/> />
</template> </template>
</tbody> </tbody>
......
<script> <script>
import { mapState, mapGetters } from 'vuex'; import { mapState } from 'vuex';
import diffDiscussions from './diff_discussions.vue'; import diffDiscussions from './diff_discussions.vue';
import diffLineNoteForm from './diff_line_note_form.vue'; import diffLineNoteForm from './diff_line_note_form.vue';
...@@ -21,48 +21,51 @@ export default { ...@@ -21,48 +21,51 @@ export default {
type: Number, type: Number,
required: true, required: true,
}, },
leftDiscussions: {
type: Array,
required: false,
default: () => [],
},
rightDiscussions: {
type: Array,
required: false,
default: () => [],
},
}, },
computed: { computed: {
...mapState({ ...mapState({
diffLineCommentForms: state => state.diffs.diffLineCommentForms, diffLineCommentForms: state => state.diffs.diffLineCommentForms,
}), }),
...mapGetters('diffs', ['discussionsByLineCode']),
leftLineCode() { leftLineCode() {
return this.line.left.lineCode; return this.line.left.lineCode;
}, },
rightLineCode() { rightLineCode() {
return this.line.right.lineCode; return this.line.right.lineCode;
}, },
hasDiscussion() {
const discussions = this.discussionsByLineCode;
return discussions[this.leftLineCode] || discussions[this.rightLineCode];
},
hasExpandedDiscussionOnLeft() { hasExpandedDiscussionOnLeft() {
const discussions = this.discussionsByLineCode[this.leftLineCode]; const discussions = this.leftDiscussions;
return discussions ? discussions.every(discussion => discussion.expanded) : false; return discussions ? discussions.every(discussion => discussion.expanded) : false;
}, },
hasExpandedDiscussionOnRight() { hasExpandedDiscussionOnRight() {
const discussions = this.discussionsByLineCode[this.rightLineCode]; const discussions = this.rightDiscussions;
return discussions ? discussions.every(discussion => discussion.expanded) : false; return discussions ? discussions.every(discussion => discussion.expanded) : false;
}, },
hasAnyExpandedDiscussion() { hasAnyExpandedDiscussion() {
return this.hasExpandedDiscussionOnLeft || this.hasExpandedDiscussionOnRight; return this.hasExpandedDiscussionOnLeft || this.hasExpandedDiscussionOnRight;
}, },
shouldRenderDiscussionsOnLeft() { shouldRenderDiscussionsOnLeft() {
return this.discussionsByLineCode[this.leftLineCode] && this.hasExpandedDiscussionOnLeft; return this.leftDiscussions && this.hasExpandedDiscussionOnLeft;
}, },
shouldRenderDiscussionsOnRight() { shouldRenderDiscussionsOnRight() {
return ( return this.rightDiscussions && this.hasExpandedDiscussionOnRight && this.line.right.type;
this.discussionsByLineCode[this.rightLineCode] && },
this.hasExpandedDiscussionOnRight && showRightSideCommentForm() {
this.line.right.type return this.line.right.type && this.diffLineCommentForms[this.rightLineCode];
);
}, },
className() { className() {
return this.hasDiscussion ? '' : 'js-temp-notes-holder'; return this.leftDiscussions.length > 0 || this.rightDiscussions.length > 0
? ''
: 'js-temp-notes-holder';
}, },
}, },
}; };
...@@ -80,13 +83,12 @@ export default { ...@@ -80,13 +83,12 @@ export default {
class="content" class="content"
> >
<diff-discussions <diff-discussions
v-if="discussionsByLineCode[leftLineCode].length" v-if="leftDiscussions.length"
:discussions="discussionsByLineCode[leftLineCode]" :discussions="leftDiscussions"
/> />
</div> </div>
<diff-line-note-form <diff-line-note-form
v-if="diffLineCommentForms[leftLineCode] && v-if="diffLineCommentForms[leftLineCode]"
diffLineCommentForms[leftLineCode]"
:diff-file-hash="diffFileHash" :diff-file-hash="diffFileHash"
:line="line.left" :line="line.left"
:note-target-line="line.left" :note-target-line="line.left"
...@@ -100,13 +102,12 @@ export default { ...@@ -100,13 +102,12 @@ export default {
class="content" class="content"
> >
<diff-discussions <diff-discussions
v-if="discussionsByLineCode[rightLineCode].length" v-if="rightDiscussions.length"
:discussions="discussionsByLineCode[rightLineCode]" :discussions="rightDiscussions"
/> />
</div> </div>
<diff-line-note-form <diff-line-note-form
v-if="diffLineCommentForms[rightLineCode] && v-if="showRightSideCommentForm"
diffLineCommentForms[rightLineCode] && line.right.type"
:diff-file-hash="diffFileHash" :diff-file-hash="diffFileHash"
:line="line.right" :line="line.right"
:note-target-line="line.right" :note-target-line="line.right"
......
...@@ -36,6 +36,16 @@ export default { ...@@ -36,6 +36,16 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
leftDiscussions: {
type: Array,
required: false,
default: () => [],
},
rightDiscussions: {
type: Array,
required: false,
default: () => [],
},
}, },
data() { data() {
return { return {
...@@ -116,6 +126,7 @@ export default { ...@@ -116,6 +126,7 @@ export default {
:is-hover="isLeftHover" :is-hover="isLeftHover"
:show-comment-button="true" :show-comment-button="true"
:diff-view-type="parallelDiffViewType" :diff-view-type="parallelDiffViewType"
:discussions="leftDiscussions"
class="diff-line-num old_line" class="diff-line-num old_line"
/> />
<td <td
...@@ -136,6 +147,7 @@ export default { ...@@ -136,6 +147,7 @@ export default {
:is-hover="isRightHover" :is-hover="isRightHover"
:show-comment-button="true" :show-comment-button="true"
:diff-view-type="parallelDiffViewType" :diff-view-type="parallelDiffViewType"
:discussions="rightDiscussions"
class="diff-line-num new_line" class="diff-line-num new_line"
/> />
<td <td
......
...@@ -21,7 +21,11 @@ export default { ...@@ -21,7 +21,11 @@ export default {
}, },
}, },
computed: { computed: {
...mapGetters('diffs', ['commitId', 'discussionsByLineCode']), ...mapGetters('diffs', [
'commitId',
'singleDiscussionByLineCode',
'shouldRenderParallelCommentRow',
]),
...mapState({ ...mapState({
diffLineCommentForms: state => state.diffs.diffLineCommentForms, diffLineCommentForms: state => state.diffs.diffLineCommentForms,
}), }),
...@@ -51,32 +55,6 @@ export default { ...@@ -51,32 +55,6 @@ export default {
return window.gon.user_color_scheme; return window.gon.user_color_scheme;
}, },
}, },
methods: {
shouldRenderCommentRow(line) {
const leftLineCode = line.left.lineCode;
const rightLineCode = line.right.lineCode;
const discussions = this.discussionsByLineCode;
const leftDiscussions = discussions[leftLineCode];
const rightDiscussions = discussions[rightLineCode];
const hasDiscussion = leftDiscussions || rightDiscussions;
const hasExpandedDiscussionOnLeft = leftDiscussions
? leftDiscussions.every(discussion => discussion.expanded)
: false;
const hasExpandedDiscussionOnRight = rightDiscussions
? rightDiscussions.every(discussion => discussion.expanded)
: false;
if (hasDiscussion && (hasExpandedDiscussionOnLeft || hasExpandedDiscussionOnRight)) {
return true;
}
const hasCommentFormOnLeft = this.diffLineCommentForms[leftLineCode];
const hasCommentFormOnRight = this.diffLineCommentForms[rightLineCode];
return hasCommentFormOnLeft || hasCommentFormOnRight;
},
},
}; };
</script> </script>
...@@ -97,13 +75,17 @@ export default { ...@@ -97,13 +75,17 @@ export default {
:line="line" :line="line"
:is-bottom="index + 1 === diffLinesLength" :is-bottom="index + 1 === diffLinesLength"
:key="index" :key="index"
:left-discussions="singleDiscussionByLineCode(line.left.lineCode)"
:right-discussions="singleDiscussionByLineCode(line.right.lineCode)"
/> />
<parallel-diff-comment-row <parallel-diff-comment-row
v-if="shouldRenderCommentRow(line)" v-if="shouldRenderParallelCommentRow(line)"
:key="`dcr-${index}`" :key="`dcr-${index}`"
:line="line" :line="line"
:diff-file-hash="diffFile.fileHash" :diff-file-hash="diffFile.fileHash"
:line-index="index" :line-index="index"
:left-discussions="singleDiscussionByLineCode(line.left.lineCode)"
:right-discussions="singleDiscussionByLineCode(line.right.lineCode)"
/> />
</template> </template>
</tbody> </tbody>
......
...@@ -75,19 +75,21 @@ export const discussionsByLineCode = (state, getters, rootState, rootGetters) => ...@@ -75,19 +75,21 @@ export const discussionsByLineCode = (state, getters, rootState, rootGetters) =>
const isDiffDiscussion = note.diff_discussion; const isDiffDiscussion = note.diff_discussion;
const hasLineCode = note.line_code; const hasLineCode = note.line_code;
const isResolvable = note.resolvable; const isResolvable = note.resolvable;
const diffRefs = diffRefsByLineCode[note.line_code];
if (isDiffDiscussion && hasLineCode && isResolvable && diffRefs) { if (isDiffDiscussion && hasLineCode && isResolvable) {
const refs = convertObjectPropsToCamelCase(note.position.formatter); const diffRefs = diffRefsByLineCode[note.line_code];
const originalRefs = convertObjectPropsToCamelCase(note.original_position.formatter); if (diffRefs) {
const refs = convertObjectPropsToCamelCase(note.position.formatter);
const originalRefs = convertObjectPropsToCamelCase(note.original_position.formatter);
if (_.isEqual(refs, diffRefs) || _.isEqual(originalRefs, diffRefs)) { if (_.isEqual(refs, diffRefs) || _.isEqual(originalRefs, diffRefs)) {
const lineCode = note.line_code; const lineCode = note.line_code;
if (acc[lineCode]) { if (acc[lineCode]) {
acc[lineCode].push(note); acc[lineCode].push(note);
} else { } else {
acc[lineCode] = [note]; acc[lineCode] = [note];
}
} }
} }
} }
...@@ -96,6 +98,47 @@ export const discussionsByLineCode = (state, getters, rootState, rootGetters) => ...@@ -96,6 +98,47 @@ export const discussionsByLineCode = (state, getters, rootState, rootGetters) =>
}, {}); }, {});
}; };
export const singleDiscussionByLineCode = (state, getters) => lineCode => {
if (!lineCode) return [];
const discussions = getters.discussionsByLineCode;
return discussions[lineCode] || [];
};
export const shouldRenderParallelCommentRow = (state, getters) => line => {
const leftLineCode = line.left.lineCode;
const rightLineCode = line.right.lineCode;
const leftDiscussions = getters.singleDiscussionByLineCode(leftLineCode);
const rightDiscussions = getters.singleDiscussionByLineCode(rightLineCode);
const hasDiscussion = leftDiscussions.length || rightDiscussions.length;
const hasExpandedDiscussionOnLeft = leftDiscussions.length
? leftDiscussions.every(discussion => discussion.expanded)
: false;
const hasExpandedDiscussionOnRight = rightDiscussions.length
? rightDiscussions.every(discussion => discussion.expanded)
: false;
if (hasDiscussion && (hasExpandedDiscussionOnLeft || hasExpandedDiscussionOnRight)) {
return true;
}
const hasCommentFormOnLeft = state.diffLineCommentForms[leftLineCode];
const hasCommentFormOnRight = state.diffLineCommentForms[rightLineCode];
return hasCommentFormOnLeft || hasCommentFormOnRight;
};
export const shouldRenderInlineCommentRow = (state, getters) => line => {
if (state.diffLineCommentForms[line.lineCode]) return true;
const lineDiscussions = getters.singleDiscussionByLineCode(line.lineCode);
if (lineDiscussions.length === 0) {
return false;
}
return lineDiscussions.every(discussion => discussion.expanded);
};
// prevent babel-plugin-rewire from generating an invalid default during karma∂ tests // prevent babel-plugin-rewire from generating an invalid default during karma∂ tests
export const getDiffFileByHash = state => fileHash => export const getDiffFileByHash = state => fileHash =>
state.diffFiles.find(file => file.fileHash === fileHash); state.diffFiles.find(file => file.fileHash === fileHash);
......
import $ from 'jquery'; import $ from 'jquery';
import { parseQueryStringIntoObject } from '~/lib/utils/common_utils'; import { parseQueryStringIntoObject } from '~/lib/utils/common_utils';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import flash from '~/flash'; import createFlash from '~/flash';
import { __ } from '~/locale'; import { __ } from '~/locale';
export default class GpgBadges { export default class GpgBadges {
static fetch() { static fetch() {
const badges = $('.js-loading-gpg-badge');
const tag = $('.js-signature-container'); const tag = $('.js-signature-container');
if (tag.length === 0) {
return Promise.resolve();
}
const badges = $('.js-loading-gpg-badge');
badges.html('<i class="fa fa-spinner fa-spin"></i>'); badges.html('<i class="fa fa-spinner fa-spin"></i>');
const displayError = () => createFlash(__('An error occurred while loading commit signatures'));
const endpoint = tag.data('signaturesPath');
if (!endpoint) {
displayError();
return Promise.reject(new Error('Missing commit signatures endpoint!'));
}
const params = parseQueryStringIntoObject(tag.serialize()); const params = parseQueryStringIntoObject(tag.serialize());
return axios.get(tag.data('signaturesPath'), { params }) return axios
.then(({ data }) => { .get(endpoint, { params })
data.signatures.forEach((signature) => { .then(({ data }) => {
badges.filter(`[data-commit-sha="${signature.commit_sha}"]`).replaceWith(signature.html); data.signatures.forEach(signature => {
}); badges.filter(`[data-commit-sha="${signature.commit_sha}"]`).replaceWith(signature.html);
}) });
.catch(() => flash(__('An error occurred while loading commits'))); })
.catch(displayError);
} }
} }
...@@ -46,7 +46,7 @@ ...@@ -46,7 +46,7 @@
by by
<a <a
:href="updatedByPath" :href="updatedByPath"
class="author_link" class="author-link"
> >
<span>{{ updatedByName }}</span> <span>{{ updatedByName }}</span>
</a> </a>
......
...@@ -541,6 +541,26 @@ export const addSelectOnFocusBehaviour = (selector = '.js-select-on-focus') => { ...@@ -541,6 +541,26 @@ export const addSelectOnFocusBehaviour = (selector = '.js-select-on-focus') => {
}); });
}; };
/**
* Method to round of values with decimal places
* with provided precision.
*
* Taken from https://stackoverflow.com/a/7343013/414749
*
* Eg; roundOffFloat(3.141592, 3) = 3.142
*
* Refer to spec/javascripts/lib/utils/common_utils_spec.js for
* more supported examples.
*
* @param {Float} number
* @param {Number} precision
*/
export const roundOffFloat = (number, precision = 0) => {
// eslint-disable-next-line no-restricted-properties
const multiplier = Math.pow(10, precision);
return Math.round(number * multiplier) / multiplier;
};
window.gl = window.gl || {}; window.gl = window.gl || {};
window.gl.utils = { window.gl.utils = {
...(window.gl.utils || {}), ...(window.gl.utils || {}),
......
...@@ -38,7 +38,7 @@ import { normalizeHeaders } from './common_utils'; ...@@ -38,7 +38,7 @@ import { normalizeHeaders } from './common_utils';
* } else { * } else {
* poll.stop(); * poll.stop();
* } * }
* }); * });
* *
* 1. Checks for response and headers before start polling * 1. Checks for response and headers before start polling
* 2. Interval is provided by `Poll-Interval` header. * 2. Interval is provided by `Poll-Interval` header.
...@@ -51,8 +51,8 @@ export default class Poll { ...@@ -51,8 +51,8 @@ export default class Poll {
constructor(options = {}) { constructor(options = {}) {
this.options = options; this.options = options;
this.options.data = options.data || {}; this.options.data = options.data || {};
this.options.notificationCallback = options.notificationCallback || this.options.notificationCallback =
function notificationCallback() {}; options.notificationCallback || function notificationCallback() {};
this.intervalHeader = 'POLL-INTERVAL'; this.intervalHeader = 'POLL-INTERVAL';
this.timeoutID = null; this.timeoutID = null;
...@@ -63,6 +63,7 @@ export default class Poll { ...@@ -63,6 +63,7 @@ export default class Poll {
const headers = normalizeHeaders(response.headers); const headers = normalizeHeaders(response.headers);
const pollInterval = parseInt(headers[this.intervalHeader], 10); const pollInterval = parseInt(headers[this.intervalHeader], 10);
if (pollInterval > 0 && response.status === httpStatusCodes.OK && this.canPoll) { if (pollInterval > 0 && response.status === httpStatusCodes.OK && this.canPoll) {
clearTimeout(this.timeoutID);
this.timeoutID = setTimeout(() => { this.timeoutID = setTimeout(() => {
this.makeRequest(); this.makeRequest();
}, pollInterval); }, pollInterval);
...@@ -77,11 +78,11 @@ export default class Poll { ...@@ -77,11 +78,11 @@ export default class Poll {
notificationCallback(true); notificationCallback(true);
return resource[method](data) return resource[method](data)
.then((response) => { .then(response => {
this.checkConditions(response); this.checkConditions(response);
notificationCallback(false); notificationCallback(false);
}) })
.catch((error) => { .catch(error => {
notificationCallback(false); notificationCallback(false);
if (error.status === httpStatusCodes.ABORTED) { if (error.status === httpStatusCodes.ABORTED) {
return; return;
......
...@@ -42,7 +42,7 @@ export default { ...@@ -42,7 +42,7 @@ export default {
by by
<a <a
:href="editedBy.path" :href="editedBy.path"
class="js-vue-author author_link"> class="js-vue-author author-link">
{{ editedBy.name }} {{ editedBy.name }}
</a> </a>
</template> </template>
......
...@@ -15,7 +15,7 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -15,7 +15,7 @@ document.addEventListener('DOMContentLoaded', () => {
const notesDataset = document.getElementById('js-vue-notes').dataset; const notesDataset = document.getElementById('js-vue-notes').dataset;
const parsedUserData = JSON.parse(notesDataset.currentUserData); const parsedUserData = JSON.parse(notesDataset.currentUserData);
const noteableData = JSON.parse(notesDataset.noteableData); const noteableData = JSON.parse(notesDataset.noteableData);
const { markdownVersion } = notesDataset; const markdownVersion = parseInt(notesDataset.markdownVersion, 10);
let currentUserData = {}; let currentUserData = {};
noteableData.noteableType = notesDataset.noteableType; noteableData.noteableType = notesDataset.noteableType;
......
...@@ -174,27 +174,19 @@ export default { ...@@ -174,27 +174,19 @@ export default {
[types.UPDATE_NOTE](state, note) { [types.UPDATE_NOTE](state, note) {
const noteObj = utils.findNoteObjectById(state.discussions, note.discussion_id); const noteObj = utils.findNoteObjectById(state.discussions, note.discussion_id);
if (noteObj.individual_note) { if (noteObj.individual_note) {
noteObj.notes.splice(0, 1, note); noteObj.notes.splice(0, 1, note);
} else { } else {
const comment = utils.findNoteObjectById(noteObj.notes, note.id); const comment = utils.findNoteObjectById(noteObj.notes, note.id);
noteObj.notes.splice(noteObj.notes.indexOf(comment), 1, note); Object.assign(comment, note);
} }
}, },
[types.UPDATE_DISCUSSION](state, noteData) { [types.UPDATE_DISCUSSION](state, noteData) {
const note = noteData; const note = noteData;
let index = 0; const selectedDiscussion = state.discussions.find(n => n.id === note.id);
state.discussions.forEach((n, i) => {
if (n.id === note.id) {
index = i;
}
});
note.expanded = true; // override expand flag to prevent collapse note.expanded = true; // override expand flag to prevent collapse
state.discussions.splice(index, 1, note); Object.assign(selectedDiscussion, note);
}, },
[types.CLOSE_ISSUE](state) { [types.CLOSE_ISSUE](state) {
...@@ -215,12 +207,9 @@ export default { ...@@ -215,12 +207,9 @@ export default {
[types.SET_DISCUSSION_DIFF_LINES](state, { discussionId, diffLines }) { [types.SET_DISCUSSION_DIFF_LINES](state, { discussionId, diffLines }) {
const discussion = utils.findNoteObjectById(state.discussions, discussionId); const discussion = utils.findNoteObjectById(state.discussions, discussionId);
const index = state.discussions.indexOf(discussion);
const discussionWithDiffLines = Object.assign({}, discussion, { Object.assign(discussion, {
truncated_diff_lines: diffLines, truncated_diff_lines: diffLines,
}); });
state.discussions.splice(index, 1, discussionWithDiffLines);
}, },
}; };
...@@ -2,13 +2,11 @@ import AjaxCache from '~/lib/utils/ajax_cache'; ...@@ -2,13 +2,11 @@ import AjaxCache from '~/lib/utils/ajax_cache';
const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm; const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm;
export const findNoteObjectById = (notes, id) => export const findNoteObjectById = (notes, id) => notes.find(n => n.id === id);
notes.filter(n => n.id === id)[0];
export const getQuickActionText = note => { export const getQuickActionText = note => {
let text = 'Applying command'; let text = 'Applying command';
const quickActions = const quickActions = AjaxCache.get(gl.GfmAutoComplete.dataSources.commands) || [];
AjaxCache.get(gl.GfmAutoComplete.dataSources.commands) || [];
const executedCommands = quickActions.filter(command => { const executedCommands = quickActions.filter(command => {
const commandRegex = new RegExp(`/${command.name}`); const commandRegex = new RegExp(`/${command.name}`);
...@@ -29,5 +27,4 @@ export const getQuickActionText = note => { ...@@ -29,5 +27,4 @@ export const getQuickActionText = note => {
export const hasQuickActions = note => REGEX_QUICK_ACTIONS.test(note); export const hasQuickActions = note => REGEX_QUICK_ACTIONS.test(note);
export const stripQuickActions = note => export const stripQuickActions = note => note.replace(REGEX_QUICK_ACTIONS, '').trim();
note.replace(REGEX_QUICK_ACTIONS, '').trim();
...@@ -2,6 +2,7 @@ import Vue from 'vue'; ...@@ -2,6 +2,7 @@ import Vue from 'vue';
import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue'; import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
import BlobViewer from '~/blob/viewer/index'; import BlobViewer from '~/blob/viewer/index';
import initBlob from '~/pages/projects/init_blob'; import initBlob from '~/pages/projects/init_blob';
import GpgBadges from '~/gpg_badges';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
new BlobViewer(); // eslint-disable-line no-new new BlobViewer(); // eslint-disable-line no-new
...@@ -26,4 +27,6 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -26,4 +27,6 @@ document.addEventListener('DOMContentLoaded', () => {
}, },
}); });
} }
GpgBadges.fetch();
}); });
...@@ -7,6 +7,7 @@ import TreeView from '~/tree'; ...@@ -7,6 +7,7 @@ import TreeView from '~/tree';
import BlobViewer from '~/blob/viewer/index'; import BlobViewer from '~/blob/viewer/index';
import Activities from '~/activities'; import Activities from '~/activities';
import { ajaxGet } from '~/lib/utils/common_utils'; import { ajaxGet } from '~/lib/utils/common_utils';
import GpgBadges from '~/gpg_badges';
import Star from '../../../star'; import Star from '../../../star';
import notificationsDropdown from '../../../notifications_dropdown'; import notificationsDropdown from '../../../notifications_dropdown';
...@@ -38,4 +39,6 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -38,4 +39,6 @@ document.addEventListener('DOMContentLoaded', () => {
$(treeSlider).waitForImages(() => { $(treeSlider).waitForImages(() => {
ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath); ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath);
}); });
GpgBadges.fetch();
}); });
...@@ -2,6 +2,7 @@ import $ from 'jquery'; ...@@ -2,6 +2,7 @@ import $ from 'jquery';
import Vue from 'vue'; import Vue from 'vue';
import initBlob from '~/blob_edit/blob_bundle'; import initBlob from '~/blob_edit/blob_bundle';
import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue'; import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
import GpgBadges from '~/gpg_badges';
import TreeView from '../../../../tree'; import TreeView from '../../../../tree';
import ShortcutsNavigation from '../../../../shortcuts_navigation'; import ShortcutsNavigation from '../../../../shortcuts_navigation';
import BlobViewer from '../../../../blob/viewer'; import BlobViewer from '../../../../blob/viewer';
...@@ -14,7 +15,8 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -14,7 +15,8 @@ document.addEventListener('DOMContentLoaded', () => {
new BlobViewer(); // eslint-disable-line no-new new BlobViewer(); // eslint-disable-line no-new
new NewCommitForm($('.js-create-dir-form')); // eslint-disable-line no-new new NewCommitForm($('.js-create-dir-form')); // eslint-disable-line no-new
$('#tree-slider').waitForImages(() => $('#tree-slider').waitForImages(() =>
ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath)); ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath),
);
initBlob(); initBlob();
const commitPipelineStatusEl = document.querySelector('.js-commit-pipeline-status'); const commitPipelineStatusEl = document.querySelector('.js-commit-pipeline-status');
...@@ -36,4 +38,6 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -36,4 +38,6 @@ document.addEventListener('DOMContentLoaded', () => {
}, },
}); });
} }
GpgBadges.fetch();
}); });
<script> <script>
import Visibility from 'visibilityjs'; import Visibility from 'visibilityjs';
import ciIcon from '~/vue_shared/components/ci_icon.vue'; import ciIcon from '~/vue_shared/components/ci_icon.vue';
import loadingIcon from '~/vue_shared/components/loading_icon.vue'; import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import Poll from '~/lib/utils/poll'; import Poll from '~/lib/utils/poll';
import Flash from '~/flash'; import Flash from '~/flash';
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import CommitPipelineService from '../services/commit_pipeline_service'; import CommitPipelineService from '../services/commit_pipeline_service';
export default { export default {
directives: { directives: {
tooltip, tooltip,
},
components: {
ciIcon,
loadingIcon,
},
props: {
endpoint: {
type: String,
required: true,
}, },
components: { /* This prop can be used to replace some of the `render_commit_status`
ciIcon,
loadingIcon,
},
props: {
endpoint: {
type: String,
required: true,
},
/* This prop can be used to replace some of the `render_commit_status`
used across GitLab, this way we could use this vue component and add a used across GitLab, this way we could use this vue component and add a
realtime status where it makes sense realtime status where it makes sense
realtime: { realtime: {
...@@ -29,76 +29,77 @@ ...@@ -29,76 +29,77 @@
required: false, required: false,
default: true, default: true,
}, */ }, */
},
data() {
return {
ciStatus: {},
isLoading: true,
};
},
computed: {
statusTitle() {
return sprintf(s__('Commits|Commit: %{commitText}'), { commitText: this.ciStatus.text });
}, },
data() { },
return { mounted() {
ciStatus: {}, this.service = new CommitPipelineService(this.endpoint);
isLoading: true, this.initPolling();
}; },
}, methods: {
computed: { successCallback(res) {
statusTitle() { const { pipelines } = res.data;
return sprintf(s__('Commits|Commit: %{commitText}'), { commitText: this.ciStatus.text }); if (pipelines.length > 0) {
}, // The pipeline entity always keeps the latest pipeline info on the `details.status`
this.ciStatus = pipelines[0].details.status;
}
this.isLoading = false;
}, },
mounted() { errorCallback() {
this.service = new CommitPipelineService(this.endpoint); this.ciStatus = {
this.initPolling(); text: 'not found',
icon: 'status_notfound',
group: 'notfound',
};
this.isLoading = false;
Flash(s__('Something went wrong on our end'));
}, },
methods: { initPolling() {
successCallback(res) { this.poll = new Poll({
const { pipelines } = res.data; resource: this.service,
if (pipelines.length > 0) { method: 'fetchData',
// The pipeline entity always keeps the latest pipeline info on the `details.status` successCallback: response => this.successCallback(response),
this.ciStatus = pipelines[0].details.status; errorCallback: this.errorCallback,
} });
this.isLoading = false;
}, if (!Visibility.hidden()) {
errorCallback() { this.isLoading = true;
this.ciStatus = { this.poll.makeRequest();
text: 'not found', } else {
icon: 'status_notfound', this.fetchPipelineCommitData();
group: 'notfound', }
};
this.isLoading = false;
Flash(s__('Something went wrong on our end'));
},
initPolling() {
this.poll = new Poll({
resource: this.service,
method: 'fetchData',
successCallback: response => this.successCallback(response),
errorCallback: this.errorCallback,
});
Visibility.change(() => {
if (!Visibility.hidden()) { if (!Visibility.hidden()) {
this.isLoading = true; this.poll.restart();
this.poll.makeRequest();
} else { } else {
this.fetchPipelineCommitData(); this.poll.stop();
} }
});
Visibility.change(() => {
if (!Visibility.hidden()) {
this.poll.restart();
} else {
this.poll.stop();
}
});
},
fetchPipelineCommitData() {
this.service.fetchData()
.then(this.successCallback)
.catch(this.errorCallback);
},
}, },
destroy() { fetchPipelineCommitData() {
this.poll.stop(); this.service
.fetchData()
.then(this.successCallback)
.catch(this.errorCallback);
}, },
}; },
destroy() {
this.poll.stop();
},
};
</script> </script>
<template> <template>
<div> <div class="ci-status-link">
<loading-icon <loading-icon
v-if="isLoading" v-if="isLoading"
label="Loading pipeline status" label="Loading pipeline status"
...@@ -113,6 +114,7 @@ ...@@ -113,6 +114,7 @@
:title="statusTitle" :title="statusTitle"
:aria-label="statusTitle" :aria-label="statusTitle"
:status="ciStatus" :status="ciStatus"
:size="24"
data-container="body" data-container="body"
/> />
</a> </a>
......
import Visibility from 'visibilityjs';
import axios from '../../lib/utils/axios_utils';
import Poll from '../../lib/utils/poll';
import * as types from './mutation_types';
export const setEndpoint = ({ commit }, endpoint) => commit(types.SET_ENDPOINT, endpoint);
export const requestReports = ({ commit }) => commit(types.REQUEST_REPORTS);
let eTagPoll;
export const clearEtagPoll = () => {
eTagPoll = null;
};
export const stopPolling = () => {
if (eTagPoll) eTagPoll.stop();
};
export const restartPolling = () => {
if (eTagPoll) eTagPoll.restart();
};
/**
* We need to poll the reports endpoint while they are being parsed in the Backend.
* This can take up to one minute.
*
* Poll.js will handle etag response.
* While http status code is 204, it means it's parsing, and we'll keep polling
* When http status code is 200, it means parsing is done, we can show the results & stop polling
* When http status code is 500, it means parsing went wrong and we stop polling
*/
export const fetchReports = ({ state, dispatch }) => {
dispatch('requestReports');
eTagPoll = new Poll({
resource: {
getReports(endpoint) {
return axios.get(endpoint);
},
},
data: state.endpoint,
method: 'getReports',
successCallback: ({ data }) => dispatch('receiveReportsSuccess', data),
errorCallback: () => dispatch('receiveReportsError'),
});
if (!Visibility.hidden()) {
eTagPoll.makeRequest();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
dispatch('restartPolling');
} else {
dispatch('stopPolling');
}
});
};
export const receiveReportsSuccess = ({ commit }, response) =>
commit(types.RECEIVE_REPORTS_SUCCESS, response);
export const receiveReportsError = ({ commit }) => commit(types.RECEIVE_REPORTS_ERROR);
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
import mutations from './mutations';
import state from './state';
Vue.use(Vuex);
export default () => new Vuex.Store({
actions,
mutations,
state: state(),
});
export const SET_ENDPOINT = 'SET_ENDPOINT';
export const REQUEST_REPORTS = 'REQUEST_REPORTS';
export const RECEIVE_REPORTS_SUCCESS = 'RECEIVE_REPORTS_SUCCESS';
export const RECEIVE_REPORTS_ERROR = 'RECEIVE_REPORTS_ERROR';
/* eslint-disable no-param-reassign */
import * as types from './mutation_types';
export default {
[types.SET_ENDPOINT](state, endpoint) {
state.endpoint = endpoint;
},
[types.REQUEST_REPORTS](state) {
state.isLoading = true;
},
[types.RECEIVE_REPORTS_SUCCESS](state, response) {
state.isLoading = false;
state.summary.total = response.summary.total;
state.summary.resolved = response.summary.resolved;
state.summary.failed = response.summary.failed;
state.reports = response.suites;
},
[types.RECEIVE_REPORTS_ERROR](state) {
state.isLoading = false;
state.hasError = true;
},
};
export default () => ({
endpoint: null,
isLoading: false,
hasError: false,
summary: {
total: 0,
resolved: 0,
failed: 0,
},
/**
* Each report will have the following format:
* {
* name: {String},
* summary: {
* total: {Number},
* resolved: {Number},
* failed: {Number},
* },
* new_failures: {Array.<Object>},
* resolved_failures: {Array.<Object>},
* existing_failures: {Array.<Object>},
* }
*/
reports: [],
});
...@@ -187,7 +187,7 @@ export default { ...@@ -187,7 +187,7 @@ export default {
<template v-else-if="hasOneUser"> <template v-else-if="hasOneUser">
<a <a
:href="assigneeUrl(firstUser)" :href="assigneeUrl(firstUser)"
class="author_link bold" class="author-link bold"
> >
<img <img
:alt="assigneeAlt(firstUser)" :alt="assigneeAlt(firstUser)"
......
...@@ -120,7 +120,7 @@ ...@@ -120,7 +120,7 @@
> >
<a <a
:href="participant.web_url" :href="participant.web_url"
class="author_link" class="author-link"
> >
<user-avatar-image <user-avatar-image
:lazy="true" :lazy="true"
......
...@@ -206,8 +206,8 @@ function UsersSelect(currentUser, els, options = {}) { ...@@ -206,8 +206,8 @@ function UsersSelect(currentUser, els, options = {}) {
return $collapsedSidebar.html(collapsedAssigneeTemplate(user)); return $collapsedSidebar.html(collapsedAssigneeTemplate(user));
}); });
}; };
collapsedAssigneeTemplate = _.template('<% if( avatar ) { %> <a class="author_link" href="/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>'); collapsedAssigneeTemplate = _.template('<% if( avatar ) { %> <a class="author-link" href="/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>');
assigneeTemplate = _.template('<% if (username) { %> <a class="author_link bold" href="/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> No assignee - <a href="#" class="js-assign-yourself"> assign yourself </a> </span> <% } %>'); assigneeTemplate = _.template('<% if (username) { %> <a class="author-link bold" href="/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> No assignee - <a href="#" class="js-assign-yourself"> assign yourself </a> </span> <% } %>');
return $dropdown.glDropdown({ return $dropdown.glDropdown({
showMenuAbove: showMenuAbove, showMenuAbove: showMenuAbove,
data: function(term, callback) { data: function(term, callback) {
......
...@@ -13,12 +13,19 @@ ...@@ -13,12 +13,19 @@
* /> * />
*/ */
import tooltip from '../directives/tooltip'; import tooltip from '../directives/tooltip';
import Icon from '../components/icon.vue';
export default { export default {
name: 'ClipboardButton', name: 'ClipboardButton',
directives: { directives: {
tooltip, tooltip,
}, },
components: {
Icon,
},
props: { props: {
text: { text: {
type: String, type: String,
...@@ -58,10 +65,6 @@ export default { ...@@ -58,10 +65,6 @@ export default {
type="button" type="button"
class="btn" class="btn"
> >
<i <icon name="duplicate" />
aria-hidden="true"
class="fa fa-clipboard"
>
</i>
</button> </button>
</template> </template>
<script> <script>
import { roundOffFloat } from '~/lib/utils/common_utils';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
export default { export default {
...@@ -70,7 +71,7 @@ export default { ...@@ -70,7 +71,7 @@ export default {
}, },
methods: { methods: {
getPercent(count) { getPercent(count) {
return Math.ceil((count / this.totalCount) * 100); return roundOffFloat((count / this.totalCount) * 100, 1);
}, },
barStyle(percent) { barStyle(percent) {
return `width: ${percent}%;`; return `width: ${percent}%;`;
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
.avatar-circle { .avatar-circle {
float: left; float: left;
margin-right: 15px; margin-right: 15px;
border-radius: $avatar_radius; border-radius: $avatar-radius;
border: 1px solid $avatar-border; border: 1px solid $avatar-border;
&.s16 { @include avatar-size(16px, 6px); } &.s16 { @include avatar-size(16px, 6px); }
&.s18 { @include avatar-size(18px, 6px); } &.s18 { @include avatar-size(18px, 6px); }
...@@ -110,7 +110,7 @@ ...@@ -110,7 +110,7 @@
color: $white-light; color: $white-light;
border: 1px solid $avatar-border; border: 1px solid $avatar-border;
border-radius: 1em; border-radius: 1em;
font-family: $regular_font; font-family: $regular-font;
font-size: 9px; font-size: 9px;
line-height: 16px; line-height: 16px;
text-align: center; text-align: center;
......
...@@ -294,6 +294,10 @@ ...@@ -294,6 +294,10 @@
.btn-clipboard { .btn-clipboard {
border: 0; border: 0;
padding: 0 5px; padding: 0 5px;
svg {
top: auto;
}
} }
.input-group-prepend, .input-group-prepend,
......
...@@ -113,8 +113,6 @@ hr { ...@@ -113,8 +113,6 @@ hr {
.item-title { font-weight: $gl-font-weight-bold; } .item-title { font-weight: $gl-font-weight-bold; }
/** FLASH message **/
.author_link,
.author-link { .author-link {
color: $gl-link-color; color: $gl-link-color;
} }
......
...@@ -80,7 +80,7 @@ label { ...@@ -80,7 +80,7 @@ label {
.form-control { .form-control {
height: 29px; height: 29px;
background: $white-light; background: $white-light;
font-family: $monospace_font; font-family: $monospace-font;
} }
.input-group-prepend .btn, .input-group-prepend .btn,
......
...@@ -9,8 +9,8 @@ ...@@ -9,8 +9,8 @@
padding: 10px 0; padding: 10px 0;
border: 0; border: 0;
border-radius: 0; border-radius: 0;
font-family: $monospace_font; font-family: $monospace-font;
font-size: $code_font_size; font-size: $code-font-size;
line-height: 19px; line-height: 19px;
margin: 0; margin: 0;
overflow: auto; overflow: auto;
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
code { code {
display: inline-block; display: inline-block;
min-width: 100%; min-width: 100%;
font-family: $monospace_font; font-family: $monospace-font;
white-space: normal; white-space: normal;
word-wrap: normal; word-wrap: normal;
padding: 0; padding: 0;
...@@ -44,7 +44,7 @@ ...@@ -44,7 +44,7 @@
float: left; float: left;
a { a {
font-family: $monospace_font; font-family: $monospace-font;
display: block; display: block;
font-size: $code_font_size !important; font-size: $code_font_size !important;
min-height: 19px; min-height: 19px;
......
.ui-widget { .ui-widget {
font-family: $regular_font; font-family: $regular-font;
font-size: $font-size-base; font-size: $font-size-base;
.ui-state-default { .ui-state-default {
......
...@@ -259,7 +259,7 @@ ul.controls { ...@@ -259,7 +259,7 @@ ul.controls {
margin-right: 0; margin-right: 0;
} }
.author_link { .author-link {
.avatar-inline { .avatar-inline {
margin-left: 0; margin-left: 0;
margin-right: 0; margin-right: 0;
...@@ -270,7 +270,7 @@ ul.controls { ...@@ -270,7 +270,7 @@ ul.controls {
.issuable-pipeline-broken a, .issuable-pipeline-broken a,
.issuable-pipeline-status a, .issuable-pipeline-status a,
.author_link { .author-link {
display: flex; display: flex;
} }
} }
......
...@@ -3,13 +3,13 @@ ...@@ -3,13 +3,13 @@
* Mixins with fixed values * Mixins with fixed values
*/ */
@mixin str-truncated($max_width: 82%) { @mixin str-truncated($max-width: 82%) {
display: inline-block; display: inline-block;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
vertical-align: top; vertical-align: top;
white-space: nowrap; white-space: nowrap;
max-width: $max_width; max-width: $max-width;
} }
/* /*
......
...@@ -33,11 +33,11 @@ ...@@ -33,11 +33,11 @@
@include media-breakpoint-up(sm) { @include media-breakpoint-up(sm) {
&:not(.wiki-sidebar):not(.build-sidebar):not(.issuable-bulk-update-sidebar) .content-wrapper { &:not(.wiki-sidebar):not(.build-sidebar):not(.issuable-bulk-update-sidebar) .content-wrapper {
padding-right: $gutter_collapsed_width; padding-right: $gutter-collapsed-width;
} }
.merge-request-tabs-holder.affix { .merge-request-tabs-holder.affix {
right: $gutter_collapsed_width; right: $gutter-collapsed-width;
} }
} }
...@@ -67,21 +67,21 @@ ...@@ -67,21 +67,21 @@
@include media-breakpoint-only(sm) { @include media-breakpoint-only(sm) {
&:not(.wiki-sidebar):not(.build-sidebar):not(.issuable-bulk-update-sidebar) .content-wrapper { &:not(.wiki-sidebar):not(.build-sidebar):not(.issuable-bulk-update-sidebar) .content-wrapper {
padding-right: $gutter_collapsed_width; padding-right: $gutter-collapsed-width;
} }
} }
@include media-breakpoint-up(md) { @include media-breakpoint-up(md) {
.content-wrapper { .content-wrapper {
padding-right: $gutter_width; padding-right: $gutter-width;
} }
&:not(.with-overlay) .merge-request-tabs-holder.affix { &:not(.with-overlay) .merge-request-tabs-holder.affix {
right: $gutter_width; right: $gutter-width;
} }
&.with-overlay .merge-request-tabs-holder.affix { &.with-overlay .merge-request-tabs-holder.affix {
right: $gutter_collapsed_width; right: $gutter-collapsed-width;
} }
} }
} }
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
.status-neutral, .status-neutral,
.status-red, { .status-red, {
height: 100%; height: 100%;
min-width: 30px; min-width: 40px;
padding: 0 5px; padding: 0 5px;
font-size: $tooltip-font-size; font-size: $tooltip-font-size;
font-weight: normal; font-weight: normal;
......
...@@ -44,7 +44,7 @@ ...@@ -44,7 +44,7 @@
// Single code lines should wrap // Single code lines should wrap
code { code {
font-family: $monospace_font; font-family: $monospace-font;
white-space: pre-wrap; white-space: pre-wrap;
word-wrap: normal; word-wrap: normal;
} }
...@@ -321,7 +321,7 @@ h6 { ...@@ -321,7 +321,7 @@ h6 {
/** CODE **/ /** CODE **/
pre { pre {
font-family: $monospace_font; font-family: $monospace-font;
display: block; display: block;
padding: $gl-padding-8; padding: $gl-padding-8;
margin: 0 0 $gl-padding-8; margin: 0 0 $gl-padding-8;
...@@ -342,7 +342,7 @@ code { ...@@ -342,7 +342,7 @@ code {
} }
.monospace { .monospace {
font-family: $monospace_font; font-family: $monospace-font;
} }
.weight-normal { .weight-normal {
...@@ -381,7 +381,7 @@ code { ...@@ -381,7 +381,7 @@ code {
* *
*/ */
textarea.js-gfm-input { textarea.js-gfm-input {
font-family: $monospace_font; font-family: $monospace-font;
font-size: 13px; font-size: 13px;
} }
......
...@@ -2,9 +2,9 @@ ...@@ -2,9 +2,9 @@
* Layout * Layout
*/ */
$grid-size: 8px; $grid-size: 8px;
$gutter_collapsed_width: 62px; $gutter-collapsed-width: 62px;
$gutter_width: 290px; $gutter-width: 290px;
$gutter_inner_width: 250px; $gutter-inner-width: 250px;
$sidebar-transition-duration: 0.3s; $sidebar-transition-duration: 0.3s;
$sidebar-breakpoint: 1024px; $sidebar-breakpoint: 1024px;
$default-transition-duration: 0.15s; $default-transition-duration: 0.15s;
...@@ -233,8 +233,8 @@ $md-area-border: #ddd; ...@@ -233,8 +233,8 @@ $md-area-border: #ddd;
/* /*
* Code * Code
*/ */
$code_font_size: 90%; $code-font-size: 90%;
$code_line_height: 1.6; $code-line-height: 1.6;
/* /*
* Tooltips * Tooltips
...@@ -371,9 +371,9 @@ $diff-jagged-border-gradient-color: darken($white-normal, 8%); ...@@ -371,9 +371,9 @@ $diff-jagged-border-gradient-color: darken($white-normal, 8%);
/* /*
* Fonts * Fonts
*/ */
$monospace_font: 'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', 'Ubuntu Mono', $monospace-font: 'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', 'Ubuntu Mono',
'Courier New', 'andale mono', 'lucida console', monospace; 'Courier New', 'andale mono', 'lucida console', monospace;
$regular_font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, $regular-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell,
'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
/* /*
...@@ -526,7 +526,7 @@ $issue-board-list-difference-md: $issue-board-list-difference-sm + $issue-boards ...@@ -526,7 +526,7 @@ $issue-board-list-difference-md: $issue-board-list-difference-sm + $issue-boards
/* /*
* Avatar * Avatar
*/ */
$avatar_radius: 50%; $avatar-radius: 50%;
$avatar-border: $gray-normal; $avatar-border: $gray-normal;
$avatar-border-hover: $gray-darker; $avatar-border-hover: $gray-darker;
$avatar-background: $gray-lightest; $avatar-background: $gray-lightest;
...@@ -830,8 +830,8 @@ $secondary: $gray-light; ...@@ -830,8 +830,8 @@ $secondary: $gray-light;
$input-disabled-bg: $gray-light; $input-disabled-bg: $gray-light;
$input-border-color: $theme-gray-200; $input-border-color: $theme-gray-200;
$input-color: $gl-text-color; $input-color: $gl-text-color;
$font-family-sans-serif: $regular_font; $font-family-sans-serif: $regular-font;
$font-family-monospace: $monospace_font; $font-family-monospace: $monospace-font;
$input-line-height: 20px; $input-line-height: 20px;
$btn-line-height: 20px; $btn-line-height: 20px;
$table-accent-bg: $gray-light; $table-accent-bg: $gray-light;
...@@ -77,13 +77,13 @@ $highlighted-gc-bg: #eaf2f5; ...@@ -77,13 +77,13 @@ $highlighted-gc-bg: #eaf2f5;
.code { .code {
background-color: $white-light; background-color: $white-light;
font-family: monospace; font-family: monospace;
font-size: $code_font_size; font-size: $code-font-size;
-premailer-cellpadding: 0; -premailer-cellpadding: 0;
-premailer-cellspacing: 0; -premailer-cellspacing: 0;
-premailer-width: 100%; -premailer-width: 100%;
> tr { > tr {
line-height: $code_line_height; line-height: $code-line-height;
} }
} }
......
...@@ -63,7 +63,7 @@ ...@@ -63,7 +63,7 @@
width: 100%; width: 100%;
&.is-compact { &.is-compact {
width: calc(100% - #{$gutter_width}); width: calc(100% - #{$gutter-width});
} }
} }
} }
......
...@@ -79,7 +79,7 @@ ...@@ -79,7 +79,7 @@
.commit-message-container { .commit-message-container {
background-color: $body-bg; background-color: $body-bg;
position: relative; position: relative;
font-family: $monospace_font; font-family: $monospace-font;
$left: 12px; $left: 12px;
overflow: hidden; // See https://gitlab.com/gitlab-org/gitlab-ce/issues/13987 overflow: hidden; // See https://gitlab.com/gitlab-org/gitlab-ce/issues/13987
.max-width-marker { .max-width-marker {
...@@ -205,7 +205,7 @@ ...@@ -205,7 +205,7 @@
> .ci-status-link, > .ci-status-link,
> .btn, > .btn,
> .commit-sha-group { > .commit-sha-group {
margin-left: $gl-padding-8; margin-left: $gl-padding;
} }
} }
...@@ -235,10 +235,6 @@ ...@@ -235,10 +235,6 @@
fill: $gl-text-color-secondary; fill: $gl-text-color-secondary;
} }
.fa-clipboard {
color: $gl-text-color-secondary;
}
:first-child { :first-child {
border-bottom-left-radius: $border-radius-default; border-bottom-left-radius: $border-radius-default;
border-top-left-radius: $border-radius-default; border-top-left-radius: $border-radius-default;
......
...@@ -368,7 +368,7 @@ ...@@ -368,7 +368,7 @@
.fa { .fa {
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
font-size: $code_font_size; font-size: $code-font-size;
} }
} }
} }
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
} }
.issue_created_ago, .issue_created_ago,
.author_link { .author-link {
white-space: nowrap; white-space: nowrap;
} }
......
...@@ -56,7 +56,7 @@ ...@@ -56,7 +56,7 @@
table { table {
width: 100%; width: 100%;
font-family: $monospace_font; font-family: $monospace-font;
border: 0; border: 0;
border-collapse: separate; border-collapse: separate;
margin: 0; margin: 0;
...@@ -73,8 +73,8 @@ ...@@ -73,8 +73,8 @@
} }
.line_holder td { .line_holder td {
line-height: $code_line_height; line-height: $code-line-height;
font-size: $code_font_size; font-size: $code-font-size;
&.noteable_line { &.noteable_line {
position: relative; position: relative;
......
...@@ -84,7 +84,7 @@ ...@@ -84,7 +84,7 @@
.soft-wrap-toggle { .soft-wrap-toggle {
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
font-family: $regular_font; font-family: $regular-font;
} }
.soft-wrap-toggle { .soft-wrap-toggle {
......
...@@ -478,7 +478,7 @@ ...@@ -478,7 +478,7 @@
} }
.deploy-info-text-link { .deploy-info-text-link {
font-family: $monospace_font; font-family: $monospace-font;
fill: $gl-link-color; fill: $gl-link-color;
&:hover { &:hover {
......
...@@ -166,7 +166,7 @@ ...@@ -166,7 +166,7 @@
border-bottom: 1px solid $border-gray-normal; border-bottom: 1px solid $border-gray-normal;
// This prevents the mess when resizing the sidebar // This prevents the mess when resizing the sidebar
// of elements repositioning themselves.. // of elements repositioning themselves..
width: $gutter_inner_width; width: $gutter-inner-width;
// -- // --
&.issuable-sidebar-header { &.issuable-sidebar-header {
...@@ -197,7 +197,7 @@ ...@@ -197,7 +197,7 @@
} }
&.assignee { &.assignee {
.author_link { .author-link {
display: block; display: block;
padding-left: 42px; padding-left: 42px;
position: relative; position: relative;
...@@ -290,7 +290,7 @@ ...@@ -290,7 +290,7 @@
} }
&.right-sidebar-expanded { &.right-sidebar-expanded {
width: $gutter_width; width: $gutter-width;
.value { .value {
line-height: 1; line-height: 1;
...@@ -377,11 +377,11 @@ ...@@ -377,11 +377,11 @@
display: block; display: block;
} }
width: $gutter_collapsed_width; width: $gutter-collapsed-width;
padding: 0; padding: 0;
.block { .block {
width: $gutter_collapsed_width - 2px; width: $gutter-collapsed-width - 2px;
padding: 15px 0 0; padding: 15px 0 0;
border-bottom: 0; border-bottom: 0;
overflow: hidden; overflow: hidden;
...@@ -486,7 +486,7 @@ ...@@ -486,7 +486,7 @@
padding-bottom: 0; padding-bottom: 0;
margin-bottom: 10px; margin-bottom: 10px;
.author_link { .author-link {
padding-left: 0; padding-left: 0;
.avatar { .avatar {
...@@ -595,7 +595,7 @@ ...@@ -595,7 +595,7 @@
margin: 16px 0 0; margin: 16px 0 0;
font-size: 85%; font-size: 85%;
.author_link { .author-link {
color: $gray-darkest; color: $gray-darkest;
} }
} }
...@@ -620,7 +620,7 @@ ...@@ -620,7 +620,7 @@
padding-right: 0; padding-right: 0;
} }
.author_link { .author-link {
display: block; display: block;
} }
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
} }
.issuable-meta { .issuable-meta {
.author_link { .author-link {
display: inline-block; display: inline-block;
} }
......
...@@ -208,7 +208,7 @@ ...@@ -208,7 +208,7 @@
position: absolute; position: absolute;
content: '...'; content: '...';
right: 0; right: 0;
font-family: $regular_font; font-family: $regular-font;
background-color: $gray-light; background-color: $gray-light;
} }
} }
......
...@@ -42,7 +42,7 @@ ...@@ -42,7 +42,7 @@
display: block; display: block;
padding: 10px 0; padding: 10px 0;
color: $gl-text-color; color: $gl-text-color;
font-family: $regular_font; font-family: $regular-font;
border: 0; border: 0;
&:focus { &:focus {
......
...@@ -328,7 +328,7 @@ ul.notes { ...@@ -328,7 +328,7 @@ ul.notes {
} }
.notes_holder { .notes_holder {
font-family: $regular_font; font-family: $regular-font;
td { td {
border: 1px solid $white-normal; border: 1px solid $white-normal;
...@@ -403,7 +403,7 @@ ul.notes { ...@@ -403,7 +403,7 @@ ul.notes {
} }
} }
.author_link { .author-link {
color: $gl-text-color; color: $gl-text-color;
} }
} }
......
...@@ -961,7 +961,7 @@ ...@@ -961,7 +961,7 @@
overflow: hidden; overflow: hidden;
.note-textarea { .note-textarea {
font-family: $monospace_font; font-family: $monospace-font;
} }
} }
......
...@@ -6,9 +6,9 @@ ...@@ -6,9 +6,9 @@
$border-style: 1px solid $border-color; $border-style: 1px solid $border-color;
font-family: $regular_font; font-family: $regular-font;
font-size: $gl-font-size; font-size: $gl-font-size;
line-height: $code_line_height; line-height: $code-line-height;
color: $gl-text-color; color: $gl-text-color;
margin: 20px; margin: 20px;
font-weight: 200; font-weight: 200;
...@@ -48,9 +48,9 @@ ...@@ -48,9 +48,9 @@
padding: 10px; padding: 10px;
border: 0; border: 0;
border-radius: 0; border-radius: 0;
font-family: $monospace_font; font-family: $monospace-font;
font-size: $code_font_size; font-size: $code-font-size;
line-height: $code_line_height; line-height: $code-line-height;
margin: 0; margin: 0;
overflow: auto; overflow: auto;
overflow-y: hidden; overflow-y: hidden;
...@@ -66,10 +66,10 @@ ...@@ -66,10 +66,10 @@
float: left; float: left;
.diff-line-num { .diff-line-num {
font-family: $monospace_font; font-family: $monospace-font;
display: block; display: block;
font-size: $code_font_size; font-size: $code-font-size;
min-height: $code_line_height; min-height: $code-line-height;
white-space: nowrap; white-space: nowrap;
color: $black-transparent; color: $black-transparent;
min-width: 30px; min-width: 30px;
......
...@@ -71,7 +71,22 @@ module LfsRequest ...@@ -71,7 +71,22 @@ module LfsRequest
def lfs_download_access? def lfs_download_access?
return false unless project.lfs_enabled? return false unless project.lfs_enabled?
ci? || lfs_deploy_token? || user_can_download_code? || build_can_download_code? ci? || lfs_deploy_token? || user_can_download_code? || build_can_download_code? || deploy_token_can_download_code?
end
def deploy_token_can_download_code?
deploy_token_present? &&
deploy_token.project == project &&
deploy_token.active? &&
deploy_token.read_repository?
end
def deploy_token_present?
user && user.is_a?(DeployToken)
end
def deploy_token
user
end end
def lfs_upload_access? def lfs_upload_access?
...@@ -86,7 +101,7 @@ module LfsRequest ...@@ -86,7 +101,7 @@ module LfsRequest
end end
def user_can_download_code? def user_can_download_code?
has_authentication_ability?(:download_code) && can?(user, :download_code, project) has_authentication_ability?(:download_code) && can?(user, :download_code, project) && !deploy_token_present?
end end
def build_can_download_code? def build_can_download_code?
......
class Import::GitlabController < Import::BaseController class Import::GitlabController < Import::BaseController
MAX_PROJECT_PAGES = 15
PER_PAGE_PROJECTS = 100
before_action :verify_gitlab_import_enabled before_action :verify_gitlab_import_enabled
before_action :gitlab_auth, except: :callback before_action :gitlab_auth, except: :callback
...@@ -10,7 +13,7 @@ class Import::GitlabController < Import::BaseController ...@@ -10,7 +13,7 @@ class Import::GitlabController < Import::BaseController
end end
def status def status
@repos = client.projects @repos = client.projects(starting_page: 1, page_limit: MAX_PROJECT_PAGES, per_page: PER_PAGE_PROJECTS)
@already_added_projects = find_already_added_projects('gitlab') @already_added_projects = find_already_added_projects('gitlab')
already_added_projects_names = @already_added_projects.pluck(:import_source) already_added_projects_names = @already_added_projects.pluck(:import_source)
......
...@@ -99,7 +99,8 @@ class ProfilesController < Profiles::ApplicationController ...@@ -99,7 +99,8 @@ class ProfilesController < Profiles::ApplicationController
:username, :username,
:website_url, :website_url,
:organization, :organization,
:preferred_language :preferred_language,
:private_profile
) )
end end
end end
...@@ -112,7 +112,7 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -112,7 +112,7 @@ class Projects::WikisController < Projects::ApplicationController
private private
def load_project_wiki def load_project_wiki
@project_wiki = ProjectWiki.new(@project, current_user) @project_wiki = load_wiki
# Call #wiki to make sure the Wiki Repo is initialized # Call #wiki to make sure the Wiki Repo is initialized
@project_wiki.wiki @project_wiki.wiki
...@@ -128,6 +128,10 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -128,6 +128,10 @@ class Projects::WikisController < Projects::ApplicationController
false false
end end
def load_wiki
ProjectWiki.new(@project, current_user)
end
def wiki_params def wiki_params
params.require(:wiki).permit(:title, :content, :format, :message, :last_commit_sha) params.require(:wiki).permit(:title, :content, :format, :message, :last_commit_sha)
end end
......
...@@ -157,6 +157,8 @@ class SessionsController < Devise::SessionsController ...@@ -157,6 +157,8 @@ class SessionsController < Devise::SessionsController
end end
def auto_sign_in_with_provider def auto_sign_in_with_provider
return unless Gitlab::Auth.omniauth_enabled?
provider = Gitlab.config.omniauth.auto_sign_in_with_provider provider = Gitlab.config.omniauth.auto_sign_in_with_provider
return unless provider.present? return unless provider.present?
......
...@@ -13,6 +13,8 @@ class UsersController < ApplicationController ...@@ -13,6 +13,8 @@ class UsersController < ApplicationController
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
before_action :user, except: [:exists] before_action :user, except: [:exists]
before_action :authorize_read_user_profile!,
only: [:calendar, :calendar_activities, :groups, :projects, :contributed_projects, :snippets]
def show def show
respond_to do |format| respond_to do |format|
...@@ -148,4 +150,8 @@ class UsersController < ApplicationController ...@@ -148,4 +150,8 @@ class UsersController < ApplicationController
def build_canonical_path(user) def build_canonical_path(user)
url_for(safe_params.merge(username: user.to_param)) url_for(safe_params.merge(username: user.to_param))
end end
def authorize_read_user_profile!
access_denied! unless can?(current_user, :read_user_profile, user)
end
end end
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
# owned: boolean # owned: boolean
# parent: Group # parent: Group
# all_available: boolean (defaults to true) # all_available: boolean (defaults to true)
# min_access_level: integer
# #
# Users with full private access can see all groups. The `owned` and `parent` # Users with full private access can see all groups. The `owned` and `parent`
# params can be used to restrict the groups that are returned. # params can be used to restrict the groups that are returned.
...@@ -39,6 +40,7 @@ class GroupsFinder < UnionFinder ...@@ -39,6 +40,7 @@ class GroupsFinder < UnionFinder
def all_groups def all_groups
return [owned_groups] if params[:owned] return [owned_groups] if params[:owned]
return [groups_with_min_access_level] if min_access_level?
return [Group.all] if current_user&.full_private_access? && all_available? return [Group.all] if current_user&.full_private_access? && all_available?
groups = [] groups = []
...@@ -56,6 +58,16 @@ class GroupsFinder < UnionFinder ...@@ -56,6 +58,16 @@ class GroupsFinder < UnionFinder
current_user.groups current_user.groups
end end
def groups_with_min_access_level
groups = current_user
.groups
.where('members.access_level >= ?', params[:min_access_level])
Gitlab::GroupHierarchy
.new(groups)
.base_and_descendants
end
def by_parent(groups) def by_parent(groups)
return groups unless params[:parent] return groups unless params[:parent]
...@@ -73,4 +85,8 @@ class GroupsFinder < UnionFinder ...@@ -73,4 +85,8 @@ class GroupsFinder < UnionFinder
def all_available? def all_available?
params.fetch(:all_available, true) params.fetch(:all_available, true)
end end
def min_access_level?
current_user && params[:min_access_level].present?
end
end end
class PersonalProjectsFinder < UnionFinder class PersonalProjectsFinder < UnionFinder
def initialize(user) include Gitlab::Allowable
def initialize(user, params = {})
@user = user @user = user
@params = params
end end
# Finds the projects belonging to the user in "@user", limited to either # Finds the projects belonging to the user in "@user", limited to either
...@@ -8,9 +11,13 @@ class PersonalProjectsFinder < UnionFinder ...@@ -8,9 +11,13 @@ class PersonalProjectsFinder < UnionFinder
# #
# current_user - When given the list of projects is limited to those only # current_user - When given the list of projects is limited to those only
# visible by this user. # visible by this user.
# params - Optional query parameters
# min_access_level: integer
# #
# Returns an ActiveRecord::Relation. # Returns an ActiveRecord::Relation.
def execute(current_user = nil) def execute(current_user = nil)
return Project.none unless can?(current_user, :read_user_profile, @user)
segments = all_projects(current_user) segments = all_projects(current_user)
find_union(segments, Project).includes(:namespace).order_updated_desc find_union(segments, Project).includes(:namespace).order_updated_desc
...@@ -19,11 +26,21 @@ class PersonalProjectsFinder < UnionFinder ...@@ -19,11 +26,21 @@ class PersonalProjectsFinder < UnionFinder
private private
def all_projects(current_user) def all_projects(current_user)
projects = [] return [projects_with_min_access_level(current_user)] if current_user && min_access_level?
projects = []
projects << @user.personal_projects.visible_to_user(current_user) if current_user projects << @user.personal_projects.visible_to_user(current_user) if current_user
projects << @user.personal_projects.public_to_user(current_user) projects << @user.personal_projects.public_to_user(current_user)
projects projects
end end
def projects_with_min_access_level(current_user)
@user
.personal_projects
.visible_to_user_and_access_level(current_user, @params[:min_access_level])
end
def min_access_level?
@params[:min_access_level].present?
end
end end
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
# search: string # search: string
# non_archived: boolean # non_archived: boolean
# archived: 'only' or boolean # archived: 'only' or boolean
# min_access_level: integer
# #
class ProjectsFinder < UnionFinder class ProjectsFinder < UnionFinder
include CustomAttributesFilter include CustomAttributesFilter
...@@ -34,7 +35,7 @@ class ProjectsFinder < UnionFinder ...@@ -34,7 +35,7 @@ class ProjectsFinder < UnionFinder
user = params.delete(:user) user = params.delete(:user)
collection = collection =
if user if user
PersonalProjectsFinder.new(user).execute(current_user) PersonalProjectsFinder.new(user, finder_params).execute(current_user)
else else
init_collection init_collection
end end
...@@ -65,6 +66,8 @@ class ProjectsFinder < UnionFinder ...@@ -65,6 +66,8 @@ class ProjectsFinder < UnionFinder
def collection_with_user def collection_with_user
if owned_projects? if owned_projects?
current_user.owned_projects current_user.owned_projects
elsif min_access_level?
current_user.authorized_projects.where('project_authorizations.access_level >= ?', params[:min_access_level])
else else
if private_only? if private_only?
current_user.authorized_projects current_user.authorized_projects
...@@ -76,7 +79,7 @@ class ProjectsFinder < UnionFinder ...@@ -76,7 +79,7 @@ class ProjectsFinder < UnionFinder
# Builds a collection for an anonymous user. # Builds a collection for an anonymous user.
def collection_without_user def collection_without_user
if private_only? || owned_projects? if private_only? || owned_projects? || min_access_level?
Project.none Project.none
else else
Project.public_to_user Project.public_to_user
...@@ -91,6 +94,10 @@ class ProjectsFinder < UnionFinder ...@@ -91,6 +94,10 @@ class ProjectsFinder < UnionFinder
params[:non_public].present? params[:non_public].present?
end end
def min_access_level?
params[:min_access_level].present?
end
def by_ids(items) def by_ids(items)
project_ids_relation ? items.where(id: project_ids_relation) : items project_ids_relation ? items.where(id: project_ids_relation) : items
end end
...@@ -143,4 +150,10 @@ class ProjectsFinder < UnionFinder ...@@ -143,4 +150,10 @@ class ProjectsFinder < UnionFinder
projects projects
end end
end end
def finder_params
return {} unless min_access_level?
{ min_access_level: params[:min_access_level] }
end
end end
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
class UserRecentEventsFinder class UserRecentEventsFinder
prepend FinderWithCrossProjectAccess prepend FinderWithCrossProjectAccess
include FinderMethods include FinderMethods
include Gitlab::Allowable
requires_cross_project_access requires_cross_project_access
...@@ -21,6 +22,8 @@ class UserRecentEventsFinder ...@@ -21,6 +22,8 @@ class UserRecentEventsFinder
end end
def execute def execute
return Event.none unless can?(current_user, :read_user_profile, target_user)
recent_events(params[:offset] || 0) recent_events(params[:offset] || 0)
.joins(:project) .joins(:project)
.with_associations .with_associations
......
...@@ -7,7 +7,7 @@ module AuthHelper ...@@ -7,7 +7,7 @@ module AuthHelper
end end
def omniauth_enabled? def omniauth_enabled?
Gitlab.config.omniauth.enabled Gitlab::Auth.omniauth_enabled?
end end
def provider_has_icon?(name) def provider_has_icon?(name)
......
...@@ -51,7 +51,7 @@ module ButtonHelper ...@@ -51,7 +51,7 @@ module ButtonHelper
} }
content_tag :button, button_attributes do content_tag :button, button_attributes do
concat(icon('clipboard', 'aria-hidden': 'true')) unless hide_button_icon concat(sprite_icon('duplicate')) unless hide_button_icon
concat(button_text) concat(button_text)
end end
end end
......
...@@ -56,7 +56,7 @@ module CiStatusHelper ...@@ -56,7 +56,7 @@ module CiStatusHelper
status.humanize status.humanize
end end
def ci_icon_for_status(status) def ci_icon_for_status(status, size: 16)
if detailed_status?(status) if detailed_status?(status)
return sprite_icon(status.icon) return sprite_icon(status.icon)
end end
...@@ -85,7 +85,7 @@ module CiStatusHelper ...@@ -85,7 +85,7 @@ module CiStatusHelper
'status_canceled' 'status_canceled'
end end
sprite_icon(icon_name, size: 16) sprite_icon(icon_name, size: size)
end end
def pipeline_status_cache_key(pipeline_status) def pipeline_status_cache_key(pipeline_status)
...@@ -111,7 +111,8 @@ module CiStatusHelper ...@@ -111,7 +111,8 @@ module CiStatusHelper
'commit', 'commit',
commit.status(ref), commit.status(ref),
path, path,
tooltip_placement: tooltip_placement) tooltip_placement: tooltip_placement,
icon_size: 24)
end end
def render_pipeline_status(pipeline, tooltip_placement: 'left') def render_pipeline_status(pipeline, tooltip_placement: 'left')
...@@ -125,16 +126,16 @@ module CiStatusHelper ...@@ -125,16 +126,16 @@ module CiStatusHelper
Ci::Runner.instance_type.blank? Ci::Runner.instance_type.blank?
end end
def render_status_with_link(type, status, path = nil, tooltip_placement: 'left', cssclass: '', container: 'body') def render_status_with_link(type, status, path = nil, tooltip_placement: 'left', cssclass: '', container: 'body', icon_size: 16)
klass = "ci-status-link ci-status-icon-#{status.dasherize} #{cssclass}" klass = "ci-status-link ci-status-icon-#{status.dasherize} #{cssclass}"
title = "#{type.titleize}: #{ci_label_for_status(status)}" title = "#{type.titleize}: #{ci_label_for_status(status)}"
data = { toggle: 'tooltip', placement: tooltip_placement, container: container } data = { toggle: 'tooltip', placement: tooltip_placement, container: container }
if path if path
link_to ci_icon_for_status(status), path, link_to ci_icon_for_status(status, size: icon_size), path,
class: klass, title: title, data: data class: klass, title: title, data: data
else else
content_tag :span, ci_icon_for_status(status), content_tag :span, ci_icon_for_status(status, size: icon_size),
class: klass, title: title, data: data class: klass, title: title, data: data
end end
end end
......
...@@ -145,15 +145,14 @@ module CommitsHelper ...@@ -145,15 +145,14 @@ module CommitsHelper
person_name person_name
end end
options = { link_options = {
class: "commit-#{options[:source]}-link has-tooltip", class: "commit-#{options[:source]}-link"
title: source_email
} }
if user.nil? if user.nil?
mail_to(source_email, text, options) mail_to(source_email, text, link_options)
else else
link_to(text, user_path(user), options) link_to(text, user_path(user), link_options)
end end
end end
......
...@@ -63,10 +63,10 @@ module ProjectsHelper ...@@ -63,10 +63,10 @@ module ProjectsHelper
author_html = author_html.html_safe author_html = author_html.html_safe
if opts[:name] if opts[:name]
link_to(author_html, user_path(author), class: "author_link #{"#{opts[:extra_class]}" if opts[:extra_class]} #{"#{opts[:mobile_classes]}" if opts[:mobile_classes]}").html_safe link_to(author_html, user_path(author), class: "author-link #{"#{opts[:extra_class]}" if opts[:extra_class]} #{"#{opts[:mobile_classes]}" if opts[:mobile_classes]}").html_safe
else else
title = opts[:title].sub(":name", sanitize(author.name)) title = opts[:title].sub(":name", sanitize(author.name))
link_to(author_html, user_path(author), class: "author_link has-tooltip", title: title, data: { container: 'body' }).html_safe link_to(author_html, user_path(author), class: "author-link has-tooltip", title: title, data: { container: 'body' }).html_safe
end end
end end
......
...@@ -42,7 +42,13 @@ module UsersHelper ...@@ -42,7 +42,13 @@ module UsersHelper
private private
def get_profile_tabs def get_profile_tabs
[:activity, :groups, :contributed, :projects, :snippets] tabs = []
if can?(current_user, :read_user_profile, @user)
tabs += [:activity, :groups, :contributed, :projects, :snippets]
end
tabs
end end
def get_current_user_menu_items def get_current_user_menu_items
......
...@@ -126,10 +126,9 @@ module VisibilityLevelHelper ...@@ -126,10 +126,9 @@ module VisibilityLevelHelper
end end
def visibility_icon_description(form_model) def visibility_icon_description(form_model)
case form_model if form_model.respond_to?(:visibility_level_allowed_as_fork?)
when Project
project_visibility_icon_description(form_model.visibility_level) project_visibility_icon_description(form_model.visibility_level)
when Group elsif form_model.respond_to?(:visibility_level_allowed_by_sub_groups?)
group_visibility_icon_description(form_model.visibility_level) group_visibility_icon_description(form_model.visibility_level)
end end
end end
......
...@@ -27,7 +27,7 @@ class DeployToken < ActiveRecord::Base ...@@ -27,7 +27,7 @@ class DeployToken < ActiveRecord::Base
end end
def active? def active?
!revoked !revoked && expires_at > Date.today
end end
def scopes def scopes
...@@ -58,6 +58,10 @@ class DeployToken < ActiveRecord::Base ...@@ -58,6 +58,10 @@ class DeployToken < ActiveRecord::Base
write_attribute(:expires_at, value.presence || Forever.date) write_attribute(:expires_at, value.presence || Forever.date)
end end
def admin?
false
end
private private
def ensure_at_least_one_scope def ensure_at_least_one_scope
......
...@@ -154,6 +154,7 @@ class Project < ActiveRecord::Base ...@@ -154,6 +154,7 @@ class Project < ActiveRecord::Base
has_one :mock_monitoring_service has_one :mock_monitoring_service
has_one :microsoft_teams_service has_one :microsoft_teams_service
has_one :packagist_service has_one :packagist_service
has_one :hangouts_chat_service
# TODO: replace these relations with the fork network versions # TODO: replace these relations with the fork network versions
has_one :forked_project_link, foreign_key: "forked_to_project_id" has_one :forked_project_link, foreign_key: "forked_to_project_id"
...@@ -326,6 +327,7 @@ class Project < ActiveRecord::Base ...@@ -326,6 +327,7 @@ class Project < ActiveRecord::Base
scope :joined, ->(user) { where('namespace_id != ?', user.namespace_id) } scope :joined, ->(user) { where('namespace_id != ?', user.namespace_id) }
scope :starred_by, ->(user) { joins(:users_star_projects).where('users_star_projects.user_id': user.id) } scope :starred_by, ->(user) { joins(:users_star_projects).where('users_star_projects.user_id': user.id) }
scope :visible_to_user, ->(user) { where(id: user.authorized_projects.select(:id).reorder(nil)) } scope :visible_to_user, ->(user) { where(id: user.authorized_projects.select(:id).reorder(nil)) }
scope :visible_to_user_and_access_level, ->(user, access_level) { where(id: user.authorized_projects.where('project_authorizations.access_level >= ?', access_level).select(:id).reorder(nil)) }
scope :archived, -> { where(archived: true) } scope :archived, -> { where(archived: true) }
scope :non_archived, -> { where(archived: false) } scope :non_archived, -> { where(archived: false) }
scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct } scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
......
require 'hangouts_chat'
class HangoutsChatService < ChatNotificationService
def title
'Hangouts Chat'
end
def description
'Receive event notifications in Google Hangouts Chat'
end
def self.to_param
'hangouts_chat'
end
def help
'This service sends notifications about projects events to Google Hangouts Chat room.<br />
To set up this service:
<ol>
<li><a href="https://developers.google.com/hangouts/chat/how-tos/webhooks">Set up an incoming webhook for your room</a>. All notifications will come to this room.</li>
<li>Paste the <strong>Webhook URL</strong> into the field below.</li>
<li>Select events below to enable notifications.</li>
</ol>'
end
def event_field(event)
end
def default_channel_placeholder
end
def webhook_placeholder
'https://chat.googleapis.com/v1/spaces…'
end
def default_fields
[
{ type: 'text', name: 'webhook', placeholder: "e.g. #{webhook_placeholder}" },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
{ type: 'checkbox', name: 'notify_only_default_branch' }
]
end
private
def notify(message, opts)
simple_text = parse_simple_text_message(message)
HangoutsChat::Sender.new(webhook).simple(simple_text)
end
def parse_simple_text_message(message)
header = message.pretext
return header if message.attachments.empty?
attachment = message.attachments.first
title = format_attachment_title(attachment)
body = attachment[:text]
[header, title, body].compact.join("\n")
end
def format_attachment_title(attachment)
return attachment[:title] unless attachment[:title_link]
"<#{attachment[:title_link]}|#{attachment[:title]}>"
end
end
...@@ -82,7 +82,7 @@ class ProjectWiki ...@@ -82,7 +82,7 @@ class ProjectWiki
# Returns an Array of Gitlab WikiPage instances or an # Returns an Array of Gitlab WikiPage instances or an
# empty Array if this Wiki has no pages. # empty Array if this Wiki has no pages.
def pages(limit: nil) def pages(limit: 0)
wiki.pages(limit: limit).map { |page| WikiPage.new(self, page, true) } wiki.pages(limit: limit).map { |page| WikiPage.new(self, page, true) }
end end
......
...@@ -254,6 +254,7 @@ class Service < ActiveRecord::Base ...@@ -254,6 +254,7 @@ class Service < ActiveRecord::Base
emails_on_push emails_on_push
external_wiki external_wiki
flowdock flowdock
hangouts_chat
hipchat hipchat
irker irker
jira jira
......
# frozen_string_literal: true
class ApplicationSetting class ApplicationSetting
class TermPolicy < BasePolicy class TermPolicy < BasePolicy
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
......
# frozen_string_literal: true
require_dependency 'declarative_policy' require_dependency 'declarative_policy'
class BasePolicy < DeclarativePolicy::Base class BasePolicy < DeclarativePolicy::Base
......
# frozen_string_literal: true
module Ci module Ci
class BuildPolicy < CommitStatusPolicy class BuildPolicy < CommitStatusPolicy
condition(:protected_ref) do condition(:protected_ref) do
......
# frozen_string_literal: true
module Ci module Ci
class PipelinePolicy < BasePolicy class PipelinePolicy < BasePolicy
delegate { @subject.project } delegate { @subject.project }
......
# frozen_string_literal: true
module Ci module Ci
class PipelineSchedulePolicy < PipelinePolicy class PipelineSchedulePolicy < PipelinePolicy
alias_method :pipeline_schedule, :subject alias_method :pipeline_schedule, :subject
......
# frozen_string_literal: true
module Ci module Ci
class RunnerPolicy < BasePolicy class RunnerPolicy < BasePolicy
with_options scope: :subject, score: 0 with_options scope: :subject, score: 0
......
# frozen_string_literal: true
module Ci module Ci
class TriggerPolicy < BasePolicy class TriggerPolicy < BasePolicy
delegate { @subject.project } delegate { @subject.project }
......
# frozen_string_literal: true
module Clusters module Clusters
class ClusterPolicy < BasePolicy class ClusterPolicy < BasePolicy
alias_method :cluster, :subject alias_method :cluster, :subject
......
# frozen_string_literal: true
class CommitStatusPolicy < BasePolicy class CommitStatusPolicy < BasePolicy
delegate { @subject.project } delegate { @subject.project }
......
# frozen_string_literal: true
class DeployKeyPolicy < BasePolicy class DeployKeyPolicy < BasePolicy
with_options scope: :subject, score: 0 with_options scope: :subject, score: 0
condition(:private_deploy_key) { @subject.private? } condition(:private_deploy_key) { @subject.private? }
......
# frozen_string_literal: true
class DeployTokenPolicy < BasePolicy class DeployTokenPolicy < BasePolicy
with_options scope: :subject, score: 0 with_options scope: :subject, score: 0
condition(:maintainer) { @subject.project.team.maintainer?(@user) } condition(:maintainer) { @subject.project.team.maintainer?(@user) }
......
# frozen_string_literal: true
class DeploymentPolicy < BasePolicy class DeploymentPolicy < BasePolicy
delegate { @subject.project } delegate { @subject.project }
end end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment