diff options
223 files changed, 3258 insertions, 1125 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 76117a48730..66a1791fc62 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -210,11 +210,12 @@ rake brakeman: *exec rake flay: *exec license_finder: *exec rake downtime_check: *exec -rake ce_to_ee_merge_check: +rake ee_compat_check: <<: *exec only: - branches except: + - master - tags allow_failure: yes @@ -279,16 +280,20 @@ bundler:audit: migration paths: stage: test <<: *use-db + variables: + SETUP_DB: "false" only: - master@gitlab-org/gitlab-ce script: - git checkout HEAD . - git fetch --tags - git checkout v8.5.9 - - 'echo test: unix:/var/opt/gitlab/redis/redis.socket > config/resque.yml' + - cp config/resque.yml.example config/resque.yml + - sed -i 's/localhost/redis/g' config/resque.yml - bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}" --retry=3 - rake db:drop db:create db:schema:load db:seed_fu - git checkout $CI_BUILD_REF + - source scripts/prepare_build.sh - rake db:migrate coverage: @@ -307,15 +312,20 @@ coverage: - coverage/assets/ # Trigger docs build +# https://gitlab.com/gitlab-com/doc-gitlab-com/blob/master/README.md#deployment-process trigger_docs: stage: post-test - before_script: [] + image: "alpine" + before_script: + - apk update && apk add curl + variables: + GIT_STRATEGY: none cache: {} artifacts: {} script: - - "curl -X POST -F token=${DOCS_TRIGGER_TOKEN} -F ref=master https://gitlab.com/api/v3/projects/38069/trigger/builds" + - "curl -X POST -F token=${DOCS_TRIGGER_TOKEN} -F ref=master -F variables[PROJECT]=ce https://gitlab.com/api/v3/projects/38069/trigger/builds" only: - - master + - master@gitlab-org/gitlab-ce # Notify slack in the end diff --git a/.scss-lint.yml b/.scss-lint.yml index 5093702519b..5c8e5ac0758 100644 --- a/.scss-lint.yml +++ b/.scss-lint.yml @@ -172,7 +172,7 @@ linters: # Split selectors onto separate lines after each comma, and have each # individual selector occupy a single line. SingleLinePerSelector: - enabled: false + enabled: true # Commas in lists should be followed by a space. SpaceAfterComma: @@ -191,7 +191,7 @@ linters: # Variables should be formatted with a single space separating the colon # from the variable's value. SpaceAfterVariableColon: - enabled: false + enabled: true # Variables should be formatted with no space between the name and the # colon. @@ -201,7 +201,7 @@ linters: # Operators should be formatted with a single space on both sides of an # infix operator. SpaceAroundOperator: - enabled: false + enabled: true # Opening braces should be preceded by a single space. SpaceBeforeBrace: @@ -223,7 +223,7 @@ linters: # Reports lines containing trailing whitespace. TrailingWhitespace: - enabled: false + enabled: true # Don't write trailing zeros for numeric values with a decimal point. TrailingZero: diff --git a/CHANGELOG.md b/CHANGELOG.md index b25431278bd..a36a6652ec8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,19 +3,50 @@ Please view this file on the master branch, on stable branches it's out of date. ## 8.14.0 (2016-11-22) - Adds user project membership expired event to clarify why user was removed (Callum Dryden) - Trim leading and trailing whitespace on project_path (Linus Thiel) + - Prevent award emoji via notes for issues/MRs authored by user (barthc) + - Adds an optional path parameter to the Commits API to filter commits by path (Luis HGO) + - Fix extra space on Build sidebar on Firefox !7060 - Fix HipChat notifications rendering (airatshigapov, eisnerd) - Add hover to trash icon in notes !7008 (blackst0ne) + - Fix sidekiq stats in admin area (blackst0ne) + - Escape ref and path for relative links !6050 (winniehell) + - Fixed link typo on /help/ui to Alerts section. !6915 (Sam Rose) + - Fix filtering of milestones with quotes in title (airatshigapov) + - Refactor less readable existance checking code from CoffeeScript !6289 (jlogandavison) - Simpler arguments passed to named_route on toggle_award_url helper method + - Fix typo in framework css class. !7086 (Daniel Voogsgerd) - Fix: Backup restore doesn't clear cache - + - API: Fix project deploy keys 400 and 500 errors when adding an existing key. !6784 (Joshua Welsh) + - Replace jquery.cookie plugin with js.cookie !7085 - Use MergeRequestsClosingIssues cache data on Issue#closed_by_merge_requests method + - Fix Sign in page 'Forgot your password?' link overlaps on medium-large screens - Fix documents and comments on Build API `scope` + - Refactor email, use setter method instead AR callbacks for email attribute (Semyon Pupkov) -## 8.13.1 (unreleased) - - Fix error in generating labels +## 8.13.1 (2016-10-25) + - Fix branch protection API. !6215 + - Fix hidden pipeline graph on commit and MR page. !6895 + - Fix Cycle analytics not showing correct data when filtering by date. !6906 + - Ensure custom provider tab labels don't break layout. !6993 + - Fix issue boards user link when in subdirectory. !7018 + - Refactor and add new environment functionality to CI yaml reference. !7026 + - Fix typo in project settings that prevents users from enabling container registry. !7037 + - Fix events order in `users/:id/events` endpoint. !7039 + - Remove extra line for empty issue description. !7045 + - Don't append issue/MR templates to any existing text. !7050 + - Fix error in generating labels. !7055 + - Stop clearing the database cache on `rake cache:clear`. !7056 + - Only show register tab if signup enabled. !7058 + - Expire and build repository cache after project import. !7064 + - Fix bug where labels would be assigned to issues that were moved. !7065 + - Fix reply-by-email not working due to queue name mismatch. !7068 + - Fix 404 for group pages when GitLab setup uses relative url. !7071 + - Fix `User#to_reference`. !7088 + - Reduce overhead of `LabelFinder` by avoiding `#presence` call. !7094 + - Fix unauthorized users dragging on issue boards. !7096 + - Only schedule `ProjectCacheWorker` jobs when needed. !7099 ## 8.13.0 (2016-10-22) - - Fix save button on project pipeline settings page. (!6955) - All Sidekiq workers now use their own queue - Avoid race condition when asynchronously removing expired artifacts. (!6881) @@ -36,7 +67,6 @@ Please view this file on the master branch, on stable branches it's out of date. - Update duration at the end of pipeline - ExpireBuildArtifactsWorker query builds table without ordering enqueuing one job per build to cleanup - Add group level labels. (!6425) - - Fix Cycle analytics not showing correct data when filtering by date. !6906 - Add an example for testing a phoenix application with Gitlab CI in the docs (Manthan Mallikarjun) - Cancelled pipelines could be retried. !6927 - Updating verbiage on git basics to be more intuitive @@ -60,6 +90,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Don't include archived projects when creating group milestones. !4940 (Jeroen Jacobs) - Add tag shortcut from the Commit page. !6543 - Keep refs for each deployment + - Close open tooltips on page navigation (Linus Thiel) - Allow browsing branches that end with '.atom' - Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller) - Replace unique keyframes mixin with keyframe mixin with specific names (ClemMakesApps) @@ -87,12 +118,14 @@ Please view this file on the master branch, on stable branches it's out of date. - Add RTL support to markdown renderer (Ebrahim Byagowi) - Add word-wrap to issue title on issue and milestone boards (ClemMakesApps) - Fix todos page mobile viewport layout (ClemMakesApps) + - Make issues search less finicky - Fix inconsistent highlighting of already selected activity nav-links (ClemMakesApps) - Remove redundant mixins (ClemMakesApps) - Added 'Download' button to the Snippets page (Justin DiPierro) - Add visibility level to project repository - Fix robots.txt disallowing access to groups starting with "s" (Matt Harrison) - Close open merge request without source project (Katarzyna Kobierska Ula Budziszewska) + - Fix showing commits from source project for merge request !6658 - Fix that manual jobs would no longer block jobs in the next stage. !6604 - Add configurable email subject suffix (Fu Xu) - Use defined colour for a language when available !6748 (nilsding) diff --git a/app/assets/javascripts/activities.js b/app/assets/javascripts/activities.js index f4f8cf04184..e9287d015c3 100644 --- a/app/assets/javascripts/activities.js +++ b/app/assets/javascripts/activities.js @@ -24,9 +24,7 @@ var filter = sender.attr("id").split("_")[0]; $('.event-filter .active').removeClass("active"); - $.cookie("event_filter", filter, { - path: gon.relative_url_root || '/' - }); + Cookies.set("event_filter", filter); sender.closest('li').toggleClass("active"); }; diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 8a61669822c..879f90bdc05 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -11,13 +11,13 @@ /*= require jquery-ui/effect-highlight */ /*= require jquery-ui/sortable */ /*= require jquery_ujs */ -/*= require jquery.cookie */ /*= require jquery.endless-scroll */ /*= require jquery.highlight */ /*= require jquery.waitforimages */ /*= require jquery.atwho */ /*= require jquery.scrollTo */ /*= require jquery.turbolinks */ +/*= require js.cookie */ /*= require turbolinks */ /*= require autosave */ /*= require bootstrap/affix */ @@ -83,14 +83,15 @@ }; // Disable button if text field is empty - window.disableButtonIfEmptyField = function(field_selector, button_selector) { + window.disableButtonIfEmptyField = function(field_selector, button_selector, event_name) { + event_name = event_name || 'input'; var closest_submit, field; field = $(field_selector); closest_submit = field.closest('form').find(button_selector); if (rstrip(field.val()) === "") { closest_submit.disable(); } - return field.on('input', function() { + return field.on(event_name, function() { if (rstrip($(this).val()) === "") { return closest_submit.disable(); } else { @@ -123,15 +124,11 @@ return str.replace(/<(?:.|\n)*?>/gm, ''); }; - window.unbindEvents = function() { - return $(document).off('scroll'); - }; - window.shiftWindow = function() { return scrollBy(0, -100); }; - document.addEventListener("page:fetch", unbindEvents); + document.addEventListener("page:fetch", gl.utils.cleanupBeforeFetch); window.addEventListener("hashchange", shiftWindow); @@ -148,6 +145,10 @@ $document = $(document); $window = $(window); $body = $('body'); + + // Set the default path for all cookies to GitLab's root directory + Cookies.defaults.path = gon.relative_url_root || '/'; + gl.utils.preventDisabledButtons(); bootstrapBreakpoint = bp.getBreakpointSize(); $(".nav-sidebar").niceScroll({ diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 44af1c135a0..be26f406763 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -91,7 +91,7 @@ css = { top: ($addBtn.offset().top + $addBtn.outerHeight()) + "px" }; - if ((position != null) && position === 'right') { + if (position === 'right') { css.left = (($addBtn.offset().left - $menu.outerWidth()) + 20) + "px"; $menu.addClass('is-aligned-right'); } else { @@ -322,21 +322,18 @@ var frequentlyUsedEmojis; frequentlyUsedEmojis = this.getFrequentlyUsedEmojis(); frequentlyUsedEmojis.push(emoji); - return $.cookie('frequently_used_emojis', frequentlyUsedEmojis.join(','), { - path: gon.relative_url_root || '/', - expires: 365 - }); + Cookies.set('frequently_used_emojis', frequentlyUsedEmojis.join(','), { expires: 365 }); }; AwardsHandler.prototype.getFrequentlyUsedEmojis = function() { var frequentlyUsedEmojis; - frequentlyUsedEmojis = ($.cookie('frequently_used_emojis') || '').split(','); + frequentlyUsedEmojis = (Cookies.get('frequently_used_emojis') || '').split(','); return _.compact(_.uniq(frequentlyUsedEmojis)); }; AwardsHandler.prototype.renderFrequentlyUsedBlock = function() { var emoji, frequentlyUsedEmojis, i, len, ul; - if ($.cookie('frequently_used_emojis')) { + if (Cookies.get('frequently_used_emojis')) { frequentlyUsedEmojis = this.getFrequentlyUsedEmojis(); ul = $("<ul class='clearfix emoji-menu-list frequent-emojis'>"); for (i = 0, len = frequentlyUsedEmojis.length; i < len; i++) { diff --git a/app/assets/javascripts/blob/template_selector.js.es6 b/app/assets/javascripts/blob/template_selector.js.es6 index 4e309e480b0..2d5c6ade053 100644 --- a/app/assets/javascripts/blob/template_selector.js.es6 +++ b/app/assets/javascripts/blob/template_selector.js.es6 @@ -68,14 +68,10 @@ // To be implemented on the extending class // e.g. // Api.gitignoreText item.name, @requestFileSuccess.bind(@) - requestFileSuccess(file, { skipFocus, append } = {}) { + requestFileSuccess(file, { skipFocus } = {}) { const oldValue = this.editor.getValue(); let newValue = file.content; - if (append && oldValue.length && oldValue !== newValue) { - newValue = oldValue + '\n\n' + newValue; - } - this.editor.setValue(newValue, 1); if (!skipFocus) this.editor.focus(); @@ -99,4 +95,3 @@ global.TemplateSelector = TemplateSelector; })(window.gl || ( window.gl = {})); - diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6 index d4f8f4b9420..56f91503017 100644 --- a/app/assets/javascripts/boards/boards_bundle.js.es6 +++ b/app/assets/javascripts/boards/boards_bundle.js.es6 @@ -5,7 +5,9 @@ //= require_tree ./stores //= require_tree ./services //= require_tree ./mixins +//= require_tree ./filters //= require ./components/board +//= require ./components/board_sidebar //= require ./components/new_list_dropdown //= require ./vue_resource_interceptor @@ -22,7 +24,8 @@ $(() => { gl.IssueBoardsApp = new Vue({ el: $boardApp, components: { - 'board': gl.issueBoards.Board + 'board': gl.issueBoards.Board, + 'board-sidebar': gl.issueBoards.BoardSidebar }, data: { state: Store.state, @@ -30,9 +33,15 @@ $(() => { endpoint: $boardApp.dataset.endpoint, boardId: $boardApp.dataset.boardId, disabled: $boardApp.dataset.disabled === 'true', - issueLinkBase: $boardApp.dataset.issueLinkBase + issueLinkBase: $boardApp.dataset.issueLinkBase, + detailIssue: Store.detail }, init: Store.create.bind(Store), + computed: { + detailIssueVisible () { + return Object.keys(this.detailIssue.issue).length; + } + }, created () { gl.boardService = new BoardService(this.endpoint, this.boardId); }, diff --git a/app/assets/javascripts/boards/components/board.js.es6 b/app/assets/javascripts/boards/components/board.js.es6 index cacb36a897f..500213a3a43 100644 --- a/app/assets/javascripts/boards/components/board.js.es6 +++ b/app/assets/javascripts/boards/components/board.js.es6 @@ -21,6 +21,7 @@ }, data () { return { + detailIssue: Store.detail, filters: Store.state.filters, showIssueForm: false }; @@ -32,6 +33,26 @@ this.list.getIssues(true); }, deep: true + }, + detailIssue: { + handler () { + if (!Object.keys(this.detailIssue.issue).length) return; + + const issue = this.list.findIssue(this.detailIssue.issue.id); + + if (issue) { + const boardsList = document.querySelectorAll('.boards-list')[0]; + const right = (this.$el.offsetLeft + this.$el.offsetWidth) - boardsList.offsetWidth; + const left = boardsList.scrollLeft - this.$el.offsetLeft; + + if (right - boardsList.scrollLeft > 0) { + boardsList.scrollLeft = right; + } else if (left > 0) { + boardsList.scrollLeft = this.$el.offsetLeft; + } + } + }, + deep: true } }, methods: { diff --git a/app/assets/javascripts/boards/components/board_card.js.es6 b/app/assets/javascripts/boards/components/board_card.js.es6 index 4a7cfeaeab2..f743d72f936 100644 --- a/app/assets/javascripts/boards/components/board_card.js.es6 +++ b/app/assets/javascripts/boards/components/board_card.js.es6 @@ -12,6 +12,17 @@ disabled: Boolean, index: Number }, + data () { + return { + showDetail: false, + detailIssue: Store.detail + }; + }, + computed: { + issueDetailVisible () { + return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id; + } + }, methods: { filterByLabel (label, e) { let labelToggleText = label.title; @@ -37,6 +48,29 @@ $('.labels-filter .dropdown-toggle-text').text(labelToggleText); Store.updateFiltersUrl(); + }, + mouseDown () { + this.showDetail = true; + }, + mouseMove () { + if (this.showDetail) { + this.showDetail = false; + } + }, + showIssue (e) { + const targetTagName = e.target.tagName.toLowerCase(); + + if (targetTagName === 'a' || targetTagName === 'button') return; + + if (this.showDetail) { + this.showDetail = false; + + if (Store.detail.issue && Store.detail.issue.id === this.issue.id) { + Store.detail.issue = {}; + } else { + Store.detail.issue = this.issue; + } + } } } }); diff --git a/app/assets/javascripts/boards/components/board_new_issue.js.es6 b/app/assets/javascripts/boards/components/board_new_issue.js.es6 index a4fad422eca..b31adfdb8da 100644 --- a/app/assets/javascripts/boards/components/board_new_issue.js.es6 +++ b/app/assets/javascripts/boards/components/board_new_issue.js.es6 @@ -1,4 +1,6 @@ (() => { + const Store = gl.issueBoards.BoardsStore; + window.gl = window.gl || {}; gl.issueBoards.BoardNewIssue = Vue.extend({ @@ -27,13 +29,16 @@ const labels = this.list.label ? [this.list.label] : []; const issue = new ListIssue({ title: this.title, - labels + labels, + subscribed: true }); this.list.newIssue(issue) .then((data) => { // Need this because our jQuery very kindly disables buttons on ALL form submissions $(this.$els.submitButton).enable(); + + Store.detail.issue = issue; }) .catch(() => { // Need this because our jQuery very kindly disables buttons on ALL form submissions diff --git a/app/assets/javascripts/boards/components/board_sidebar.js.es6 b/app/assets/javascripts/boards/components/board_sidebar.js.es6 new file mode 100644 index 00000000000..e83e69247d9 --- /dev/null +++ b/app/assets/javascripts/boards/components/board_sidebar.js.es6 @@ -0,0 +1,52 @@ +(() => { + const Store = gl.issueBoards.BoardsStore; + + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + + gl.issueBoards.BoardSidebar = Vue.extend({ + props: { + currentUser: Object + }, + data() { + return { + detail: Store.detail, + issue: {} + }; + }, + computed: { + showSidebar () { + return Object.keys(this.issue).length; + } + }, + watch: { + detail: { + handler () { + this.issue = this.detail.issue; + }, + deep: true + }, + issue () { + if (this.showSidebar) { + this.$nextTick(() => { + $('.right-sidebar').getNiceScroll(0).doScrollTop(0, 0); + $('.right-sidebar').getNiceScroll().resize(); + }); + } + } + }, + methods: { + closeSidebar () { + this.detail.issue = {}; + } + }, + ready () { + new IssuableContext(this.currentUser); + new MilestoneSelect(); + new gl.DueDateSelectors(); + new LabelsSelect(); + new Sidebar(); + new Subscription('.subscription'); + } + }); +})(); diff --git a/app/assets/javascripts/boards/filters/due_date_filters.js.es6 b/app/assets/javascripts/boards/filters/due_date_filters.js.es6 new file mode 100644 index 00000000000..50ef1911022 --- /dev/null +++ b/app/assets/javascripts/boards/filters/due_date_filters.js.es6 @@ -0,0 +1,4 @@ +Vue.filter('due-date', (value) => { + const date = new Date(value); + return $.datepicker.formatDate('M d, yy', date); +}); diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 index f629d45c587..bd9ba7d5118 100644 --- a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 +++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 @@ -22,7 +22,7 @@ fallbackOnBody: true, ghostClass: 'is-ghost', filter: '.has-tooltip, .btn', - delay: gl.issueBoards.touchEnabled ? 100 : 0, + delay: gl.issueBoards.touchEnabled ? 100 : 50, scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100, scrollSpeed: 20, onStart: gl.issueBoards.onStart, diff --git a/app/assets/javascripts/boards/models/issue.js.es6 b/app/assets/javascripts/boards/models/issue.js.es6 index eb082103de9..03fd6c05d87 100644 --- a/app/assets/javascripts/boards/models/issue.js.es6 +++ b/app/assets/javascripts/boards/models/issue.js.es6 @@ -3,12 +3,18 @@ class ListIssue { this.id = obj.iid; this.title = obj.title; this.confidential = obj.confidential; + this.dueDate = obj.due_date; + this.subscribed = obj.subscribed; this.labels = []; if (obj.assignee) { this.assignee = new ListUser(obj.assignee); } + if (obj.milestone) { + this.milestone = new ListMilestone(obj.milestone); + } + obj.labels.forEach((label) => { this.labels.push(new ListLabel(label)); }); @@ -41,4 +47,21 @@ class ListIssue { getLists () { return gl.issueBoards.BoardsStore.state.lists.filter( list => list.findIssue(this.id) ); } + + update (url) { + const data = { + issue: { + milestone_id: this.milestone ? this.milestone.id : null, + due_date: this.dueDate, + assignee_id: this.assignee ? this.assignee.id : null, + label_ids: this.labels.map( (label) => label.id ) + } + }; + + if (!data.issue.label_ids.length) { + data.issue.label_ids = ['']; + } + + return Vue.http.patch(url, data); + } } diff --git a/app/assets/javascripts/boards/models/milestone.js.es6 b/app/assets/javascripts/boards/models/milestone.js.es6 new file mode 100644 index 00000000000..577adf11265 --- /dev/null +++ b/app/assets/javascripts/boards/models/milestone.js.es6 @@ -0,0 +1,6 @@ +class ListMilestone { + constructor (obj) { + this.id = obj.id; + this.title = obj.title; + } +} diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6 index b9c91cbf31e..b76063911bb 100644 --- a/app/assets/javascripts/boards/services/board_service.js.es6 +++ b/app/assets/javascripts/boards/services/board_service.js.es6 @@ -1,7 +1,5 @@ class BoardService { constructor (root, boardId) { - Vue.http.options.root = root; - this.lists = Vue.resource(`${root}/${boardId}/lists{/id}`, {}, { generate: { method: 'POST', diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6 index bd07ee0c161..c30f66e3a5a 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js.es6 +++ b/app/assets/javascripts/boards/stores/boards_store.js.es6 @@ -5,6 +5,9 @@ gl.issueBoards.BoardsStore = { disabled: false, state: {}, + detail: { + issue: {} + }, moving: { issue: {}, list: {} @@ -58,12 +61,12 @@ removeBlankState () { this.removeList('blank'); - $.cookie('issue_board_welcome_hidden', 'true', { + Cookies.set('issue_board_welcome_hidden', 'true', { expires: 365 * 10 }); }, welcomeIsHidden () { - return $.cookie('issue_board_welcome_hidden') === 'true'; + return Cookies.get('issue_board_welcome_hidden') === 'true'; }, removeList (id, type = 'blank') { const list = this.findList('id', id, type); diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index 97462a5959c..f4c387a1a05 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -148,7 +148,7 @@ }; Build.prototype.translateSidebar = function(e) { - var newPosition = this.sidebarTranslationLimits.max - document.body.scrollTop; + var newPosition = this.sidebarTranslationLimits.max - (document.body.scrollTop || document.documentElement.scrollTop); if (newPosition < this.sidebarTranslationLimits.min) newPosition = this.sidebarTranslationLimits.min; this.$sidebar.css({ top: newPosition diff --git a/app/assets/javascripts/cycle_analytics.js.es6 b/app/assets/javascripts/cycle_analytics.js.es6 index 20791bab942..43c60d25248 100644 --- a/app/assets/javascripts/cycle_analytics.js.es6 +++ b/app/assets/javascripts/cycle_analytics.js.es6 @@ -6,7 +6,7 @@ const store = gl.cycleAnalyticsStore = { isLoading: true, hasError: false, - isHelpDismissed: $.cookie(COOKIE_NAME), + isHelpDismissed: Cookies.get(COOKIE_NAME), analytics: {} }; @@ -75,9 +75,7 @@ dismissLanding() { store.isHelpDismissed = true; - $.cookie(COOKIE_NAME, true, { - path: gon.relative_url_root || '/' - }); + Cookies.set(COOKIE_NAME, true); } initDropdown() { diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index afc0d6f8c62..a1fe57562fa 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -117,6 +117,9 @@ new ZenMode(); shortcut_handler = new ShortcutsNavigation(); break; + case 'projects:commit:builds': + new gl.Pipelines(); + break; case 'projects:commits:show': case 'projects:activity': shortcut_handler = new ShortcutsNavigation(); diff --git a/app/assets/javascripts/due_date_select.js.es6 b/app/assets/javascripts/due_date_select.js.es6 index 41925fcc8e3..27a7b95e281 100644 --- a/app/assets/javascripts/due_date_select.js.es6 +++ b/app/assets/javascripts/due_date_select.js.es6 @@ -41,7 +41,12 @@ defaultDate: $("input[name='" + this.fieldName + "']").val(), altField: "input[name='" + this.fieldName + "']", onSelect: () => { - return this.saveDueDate(true); + if (this.$dropdown.hasClass('js-issue-boards-due-date')) { + gl.issueBoards.BoardsStore.detail.issue.dueDate = $(`input[name='${this.fieldName}']`).val(); + this.updateIssueBoardIssue(); + } else { + return this.saveDueDate(true); + } } }); } @@ -49,8 +54,14 @@ initRemoveDueDate() { this.$block.on('click', '.js-remove-due-date', (e) => { e.preventDefault(); - $("input[name='" + this.fieldName + "']").val(''); - return this.saveDueDate(false); + + if (this.$dropdown.hasClass('js-issue-boards-due-date')) { + gl.issueBoards.BoardsStore.detail.issue.dueDate = ''; + this.updateIssueBoardIssue(); + } else { + $("input[name='" + this.fieldName + "']").val(''); + return this.saveDueDate(false); + } }); } @@ -83,6 +94,18 @@ this.datePayload = datePayload; } + updateIssueBoardIssue () { + this.$loading.fadeIn(); + this.$dropdown.trigger('loading.gl.dropdown'); + this.$selectbox.hide(); + this.$value.css('display', ''); + + gl.issueBoards.BoardsStore.detail.issue.update(this.$dropdown.attr('data-issue-update')) + .then(() => { + this.$loading.fadeOut(); + }); + } + submitSelectedDate(isDropdown) { return $.ajax({ type: 'PUT', diff --git a/app/assets/javascripts/extensions/element.js.es6 b/app/assets/javascripts/extensions/element.js.es6 new file mode 100644 index 00000000000..d5d4af3573c --- /dev/null +++ b/app/assets/javascripts/extensions/element.js.es6 @@ -0,0 +1,6 @@ +Element.prototype.matches = Element.prototype.matches || Element.prototype.msMatches; + +Element.prototype.closest = function closest(selector, selectedElement = this) { + if (!selectedElement) return; + return selectedElement.matches(selector) ? selectedElement : Element.prototype.closest(selector, selectedElement.parentElement); +}; diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 53762f2965c..4adcd00f716 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -208,7 +208,7 @@ FILTER_INPUT = '.dropdown-input .dropdown-input-field'; function GitLabDropdown(el1, options) { - var ref, ref1, ref2, ref3, searchFields, selector, self; + var searchFields, selector, self; this.el = el1; this.options = options; this.updateLabel = bind(this.updateLabel, this); @@ -219,7 +219,11 @@ selector = $(this.el).data("target"); this.dropdown = selector != null ? $(selector) : $(this.el).parent(); // Set Defaults - ref = this.options, this.filterInput = (ref1 = ref.filterInput) != null ? ref1 : this.getElement(FILTER_INPUT), this.highlight = (ref2 = ref.highlight) != null ? ref2 : false, this.filterInputBlur = (ref3 = ref.filterInputBlur) != null ? ref3 : true; + this.filterInput = this.options.filterInput || this.getElement(FILTER_INPUT); + this.highlight = !!this.options.highlight + this.filterInputBlur = this.options.filterInputBlur != null + ? this.options.filterInputBlur + : true; // If no input is passed create a default one self = this; // If selector was passed @@ -418,7 +422,9 @@ var $target; if (this.options.multiSelect) { $target = $(e.target); - if ($target && !$target.hasClass('dropdown-menu-close') && !$target.hasClass('dropdown-menu-close-icon') && !$target.data('is-link')) { + if ($target && !$target.hasClass('dropdown-menu-close') && + !$target.hasClass('dropdown-menu-close-icon') && + !$target.data('is-link')) { e.stopPropagation(); return false; } else { @@ -549,6 +555,8 @@ value = this.options.id ? this.options.id(data) : data.id; fieldName = this.options.fieldName; + if (value) { value = value.toString().replace(/'/g, '\\\'') }; + field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']"); if (field.length) { selected = true; @@ -620,8 +628,21 @@ selectedObject = this.renderedData[selectedIndex]; } } + + if (this.options.vue) { + if (el.hasClass(ACTIVE_CLASS)) { + el.removeClass(ACTIVE_CLASS); + } else { + el.addClass(ACTIVE_CLASS); + } + + return selectedObject; + } + field = []; - value = this.options.id ? this.options.id(selectedObject, el) : selectedObject.id; + value = this.options.id + ? this.options.id(selectedObject, el) + : selectedObject.id; if (isInput) { field = $(this.el); } else if(value) { diff --git a/app/assets/javascripts/gl_field_errors.js.es6 b/app/assets/javascripts/gl_field_errors.js.es6 index 8657e7b4abf..8e8f9f29ab3 100644 --- a/app/assets/javascripts/gl_field_errors.js.es6 +++ b/app/assets/javascripts/gl_field_errors.js.es6 @@ -137,8 +137,11 @@ } initValidators () { - // select all non-hidden inputs in form - this.state.inputs = this.form.find(':input:not([type=hidden])').toArray() + // register selectors here as needed + const validateSelectors = [':text', ':password', '[type=email]'] + .map((selector) => `input${selector}`).join(','); + + this.state.inputs = this.form.find(validateSelectors).toArray() .filter((input) => !input.classList.contains(customValidationFlag)) .map((input) => new GlFieldError({ input, formErrors: this })); diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js index 7d9d4d7c679..917c87a21a0 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js @@ -29,8 +29,7 @@ ContributorsGraph.set_y_domain = function(data) { return ContributorsGraph.prototype.y_domain = [ 0, d3.max(data, function(d) { - var ref, ref1; - return d.commits = (ref = (ref1 = d.commits) != null ? ref1 : d.additions) != null ? ref : d.deletions; + return d.commits = d.commits || d.additions || d.deletions; }) ]; }; @@ -44,8 +43,7 @@ ContributorsGraph.init_y_domain = function(data) { return ContributorsGraph.prototype.y_domain = [ 0, d3.max(data, function(d) { - var ref, ref1; - return d.commits = (ref = (ref1 = d.commits) != null ? ref1 : d.additions) != null ? ref : d.deletions; + return d.commits = d.commits || d.additions || d.deletions; }) ]; }; @@ -147,9 +145,8 @@ return this.area = d3.svg.area().x(function(d) { return x(d.date); }).y0(this.height).y1(function(d) { - var ref, ref1, xa; - xa = d.commits = (ref = (ref1 = d.commits) != null ? ref1 : d.additions) != null ? ref : d.deletions; - return y(xa); + d.commits = d.commits || d.additions || d.deletions; + return y(d.commits); }).interpolate("basis"); }; diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6 index 57f7e4ef230..54056ac50c8 100644 --- a/app/assets/javascripts/issuable.js.es6 +++ b/app/assets/javascripts/issuable.js.es6 @@ -15,16 +15,61 @@ return Issuable.labelRow = _.template('<% _.each(labels, function(label){ %> <span class="label-row btn-group" role="group" aria-label="<%- label.title %>" style="color: <%- label.text_color %>;"> <a href="#" class="btn btn-transparent has-tooltip" style="background-color: <%- label.color %>;" title="<%- label.description %>" data-container="body"> <%- label.title %> </a> <button type="button" class="btn btn-transparent label-remove js-label-filter-remove" style="background-color: <%- label.color %>;" data-label="<%- label.title %>"> <i class="fa fa-times"></i> </button> </span> <% }); %>'); }, initSearch: function() { + const $searchInput = $('#issuable_search'); + + Issuable.initSearchState($searchInput); + // `immediate` param set to false debounces on the `trailing` edge, lets user finish typing - const debouncedExecSearch = _.debounce(Issuable.executeSearch, 500, false); + const debouncedExecSearch = _.debounce(Issuable.executeSearch, 1000, false); - $('#issuable_search').off('keyup').on('keyup', debouncedExecSearch); + $searchInput.off('keyup').on('keyup', debouncedExecSearch); // ensures existing filters are preserved when manually submitted - $('#issue_search_form').on('submit', (e) => { + $('#issuable_search_form').on('submit', (e) => { e.preventDefault(); debouncedExecSearch(e); }); + + }, + initSearchState: function($searchInput) { + const currentSearchVal = $searchInput.val(); + + Issuable.searchState = { + elem: $searchInput, + current: currentSearchVal + }; + + Issuable.maybeFocusOnSearch(); + }, + accessSearchPristine: function(set) { + // store reference to previous value to prevent search on non-mutating keyup + const state = Issuable.searchState; + const currentSearchVal = state.elem.val(); + + if (set) { + state.current = currentSearchVal; + } else { + return state.current === currentSearchVal; + } + }, + maybeFocusOnSearch: function() { + const currentSearchVal = Issuable.searchState.current; + if (currentSearchVal && currentSearchVal !== '') { + const queryLength = currentSearchVal.length; + const $searchInput = Issuable.searchState.elem; + + /* The following ensures that the cursor is initially placed at + * the end of search input when focus is applied. It accounts + * for differences in browser implementations of `setSelectionRange` + * and cursor placement for elements in focus. + */ + $searchInput.focus(); + if ($searchInput.setSelectionRange) { + $searchInput.setSelectionRange(queryLength, queryLength); + } else { + $searchInput.val(currentSearchVal); + } + } }, executeSearch: function(e) { const $search = $('#issuable_search'); @@ -32,6 +77,11 @@ const $searchValue = $search.val(); const $filtersForm = $('.js-filter-form'); const $input = $(`input[name='${$searchName}']`, $filtersForm); + const isPristine = Issuable.accessSearchPristine(); + + if (isPristine) { + return; + } if (!$input.length) { $filtersForm.append(`<input type='hidden' name='${$searchName}' value='${_.escape($searchValue)}'/>`); diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js index b7f92ae9883..15cfa756425 100644 --- a/app/assets/javascripts/issuable_form.js +++ b/app/assets/javascripts/issuable_form.js @@ -44,8 +44,8 @@ }; IssuableForm.prototype.handleSubmit = function() { - var ref, ref1; - if (((ref = parseInt((ref1 = this.issueMoveField) != null ? ref1.val() : void 0)) != null ? ref : 0) > 0) { + var fieldId = (this.issueMoveField != null) ? this.issueMoveField.val() : null; + if ((parseInt(fieldId) || 0) > 0) { if (!confirm(this.issueMoveConfirmMsg)) { return false; } diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index b4f6e70f694..4e29ab71bd4 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -22,7 +22,7 @@ abilityName = $dropdown.data('ability-name'); $selectbox = $dropdown.closest('.selectbox'); $block = $selectbox.closest('.block'); - $form = $dropdown.closest('form'); + $form = $dropdown.closest('form, .js-issuable-update'); $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span'); $sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip'); $value = $block.find('.value'); @@ -317,6 +317,7 @@ } }, multiSelect: $dropdown.hasClass('js-multiselect'), + vue: $dropdown.hasClass('js-issue-board-sidebar'), clicked: function(label, $el, e) { var isIssueIndex, isMRIndex, page; _this.enableBulkLabelDropdown(); @@ -334,7 +335,7 @@ page = $('body').data('page'); isIssueIndex = page === 'projects:issues:index'; isMRIndex = page === 'projects:merge_requests:index'; - if ($('html').hasClass('issue-boards-page')) { + if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) { if (label.isAny) { gl.issueBoards.BoardsStore.state.filters['label_name'] = []; } @@ -362,6 +363,30 @@ else if ($dropdown.hasClass('js-filter-submit')) { return $dropdown.closest('form').submit(); } + else if ($dropdown.hasClass('js-issue-board-sidebar')) { + if ($el.hasClass('is-active')) { + gl.issueBoards.BoardsStore.detail.issue.labels.push(new ListLabel({ + id: label.id, + title: label.title, + color: label.color[0], + textColor: '#fff' + })); + } + else { + var labels = gl.issueBoards.BoardsStore.detail.issue.labels; + labels = labels.filter(function (selectedLabel) { + return selectedLabel.id !== label.id; + }); + gl.issueBoards.BoardsStore.detail.issue.labels = labels; + } + + $loading.fadeIn(); + + gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update')) + .then(function () { + $loading.fadeOut(); + }); + } else { if ($dropdown.hasClass('js-multiselect')) { diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index b170e26eebf..698abae6228 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -43,6 +43,14 @@ parser.href = url; return parser; }; + + gl.utils.cleanupBeforeFetch = function() { + // Unbind scroll events + $(document).off('scroll'); + // Close any open tooltips + $('.has-tooltip, [data-toggle="tooltip"]').tooltip('destroy'); + }; + return jQuery.timefor = function(time, suffix, expiredLabel) { var suffixFromNow, timefor; if (!time) { diff --git a/app/assets/javascripts/members.js.es6 b/app/assets/javascripts/members.js.es6 index a0cd20f21e8..2bdd0f7a637 100644 --- a/app/assets/javascripts/members.js.es6 +++ b/app/assets/javascripts/members.js.es6 @@ -10,6 +10,7 @@ $('.project_member, .group_member').off('ajax:success').on('ajax:success', this.removeRow); $('.js-member-update-control').off('change').on('change', this.formSubmit); $('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess); + disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change'); } removeRow(e) { diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 index 5c5c65f29d4..791db57262f 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 +++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 @@ -1,7 +1,7 @@ ((global) => { global.mergeConflicts = global.mergeConflicts || {}; - const diffViewType = $.cookie('diff_view'); + const diffViewType = Cookies.get('diff_view'); const HEAD_HEADER_TEXT = 'HEAD//our changes'; const ORIGIN_HEADER_TEXT = 'origin//their changes'; const HEAD_BUTTON_TITLE = 'Use ours'; @@ -180,9 +180,7 @@ this.state.diffView = viewType; this.state.isParallel = viewType === VIEW_TYPES.PARALLEL; - $.cookie('diff_view', viewType, { - path: gon.relative_url_root || '/' - }); + Cookies.set('diff_view', viewType); }, getHeadHeaderLine(id) { diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 9f28738e06b..18cc4d4ca93 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -3,7 +3,7 @@ // Handles persisting and restoring the current tab selection and lazily-loading // content on the MergeRequests#show page. // -/*= require jquery.cookie */ +/*= require js.cookie */ // // ### Example Markup @@ -282,6 +282,7 @@ document.querySelector("div#builds").innerHTML = data.html; gl.utils.localTimeAgo($('.js-timeago', 'div#builds')); _this.buildsLoaded = true; + if (!this.pipelines) this.pipelines = new gl.Pipelines(); return _this.scrollToElement("#builds"); }; })(this) @@ -367,7 +368,7 @@ MergeRequestTabs.prototype.expandView = function() { var $gutterIcon; - if ($.cookie('collapsed_gutter') === 'true') { + if (Cookies.get('collapsed_gutter') === 'true') { return; } $gutterIcon = $('.js-sidebar-toggle i:visible'); diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index cee42633c79..95b5b934c81 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -101,6 +101,7 @@ // display:block overrides the hide-collapse rule return $value.css('display', ''); }, + vue: $dropdown.hasClass('js-issue-board-sidebar'), clicked: function(selected, $el, e) { var data, isIssueIndex, isMRIndex, page; page = $('body').data('page'); @@ -110,7 +111,7 @@ e.preventDefault(); return; } - if ($('html').hasClass('issue-boards-page')) { + if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) { gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = selected.name; gl.issueBoards.BoardsStore.updateFiltersUrl(); e.preventDefault(); @@ -123,6 +124,24 @@ return Issuable.filterResults($dropdown.closest('form')); } else if ($dropdown.hasClass('js-filter-submit')) { return $dropdown.closest('form').submit(); + } else if ($dropdown.hasClass('js-issue-board-sidebar')) { + if (selected.id !== -1) { + Vue.set(gl.issueBoards.BoardsStore.detail.issue, 'milestone', new ListMilestone({ + id: selected.id, + title: selected.name + })); + } else { + Vue.delete(gl.issueBoards.BoardsStore.detail.issue, 'milestone'); + } + + $dropdown.trigger('loading.gl.dropdown'); + $loading.fadeIn(); + + gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update')) + .then(function () { + $dropdown.trigger('loaded.gl.dropdown'); + $loading.fadeOut(); + }); } else { selected = $selectbox.find('input[type="hidden"]').val(); data = {}; diff --git a/app/assets/javascripts/pipelines.js.es6 b/app/assets/javascripts/pipelines.js.es6 index a7624de6089..0fa56df0d2a 100644 --- a/app/assets/javascripts/pipelines.js.es6 +++ b/app/assets/javascripts/pipelines.js.es6 @@ -2,36 +2,39 @@ class Pipelines { constructor() { - $(document).off('click', '.toggle-pipeline-btn').on('click', '.toggle-pipeline-btn', this.toggleGraph); + this.initGraphToggle(); this.addMarginToBuildColumns(); } - toggleGraph() { - const $pipelineBtn = $(this).closest('.toggle-pipeline-btn'); - const $pipelineGraph = $(this).closest('.row-content-block').next('.pipeline-graph'); - const $btnText = $(this).find('.toggle-btn-text'); - const graphCollapsed = $pipelineGraph.hasClass('graph-collapsed'); - - $($pipelineBtn).add($pipelineGraph).toggleClass('graph-collapsed'); - + initGraphToggle() { + this.pipelineGraph = document.querySelector('.pipeline-graph'); + this.toggleButton = document.querySelector('.toggle-pipeline-btn'); + this.toggleButtonText = this.toggleButton.querySelector('.toggle-btn-text'); + this.toggleButton.addEventListener('click', this.toggleGraph.bind(this)); + } - graphCollapsed ? $btnText.text('Hide') : $btnText.text('Expand') + toggleGraph() { + const graphCollapsed = this.pipelineGraph.classList.contains('graph-collapsed'); + this.toggleButton.classList.toggle('graph-collapsed'); + this.pipelineGraph.classList.toggle('graph-collapsed'); + this.toggleButtonText.textContent = graphCollapsed ? 'Hide' : 'Expand'; } addMarginToBuildColumns() { - const $secondChildBuildNode = $('.build:nth-child(2)'); - if ($secondChildBuildNode.length) { - const $firstChildBuildNode = $secondChildBuildNode.prev('.build'); - const $multiBuildColumn = $secondChildBuildNode.closest('.stage-column'); - const $previousColumn = $multiBuildColumn.prev('.stage-column'); - $multiBuildColumn.addClass('left-margin'); - $firstChildBuildNode.addClass('left-connector'); - $previousColumn.each(function() { - $this = $(this); - if ($('.build', $this).length === 1) $this.addClass('no-margin'); - }); + const secondChildBuildNodes = this.pipelineGraph.querySelectorAll('.build:nth-child(2)'); + for (buildNodeIndex in secondChildBuildNodes) { + const buildNode = secondChildBuildNodes[buildNodeIndex]; + const firstChildBuildNode = buildNode.previousElementSibling; + if (!firstChildBuildNode || !firstChildBuildNode.matches('.build')) continue; + const multiBuildColumn = buildNode.closest('.stage-column'); + const previousColumn = multiBuildColumn.previousElementSibling; + if (!previousColumn || !previousColumn.matches('.stage-column')) continue; + multiBuildColumn.classList.add('left-margin'); + firstChildBuildNode.classList.add('left-connector'); + const columnBuilds = previousColumn.querySelectorAll('.build'); + if (columnBuilds.length === 1) previousColumn.classList.add('no-margin'); } - $('.pipeline-graph').removeClass('hidden'); + this.pipelineGraph.classList.remove('hidden'); } } diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js index a6c015299a0..61c33dc1f20 100644 --- a/app/assets/javascripts/project.js +++ b/app/assets/javascripts/project.js @@ -23,16 +23,12 @@ return $(this).parents('form').submit(); }); $('.hide-no-ssh-message').on('click', function(e) { - $.cookie('hide_no_ssh_message', 'false', { - path: gon.relative_url_root || '/' - }); + Cookies.set('hide_no_ssh_message', 'false'); $(this).parents('.no-ssh-key-message').remove(); return e.preventDefault(); }); $('.hide-no-password-message').on('click', function(e) { - $.cookie('hide_no_password_message', 'false', { - path: gon.relative_url_root || '/' - }); + Cookies.set('hide_no_password_message', 'false'); $(this).parents('.no-password-message').remove(); return e.preventDefault(); }); @@ -82,7 +78,7 @@ if (ref.header != null) { return $('<li />').addClass('dropdown-header').text(ref.header); } else { - link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', escape(ref)); + link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', ref); return $('<li />').append(link); } }, diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index e3d5f413c77..870ddfe05f1 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -5,15 +5,24 @@ function Sidebar(currentUser) { this.toggleTodo = bind(this.toggleTodo, this); this.sidebar = $('aside'); + this.removeListeners(); this.addEventListeners(); } + Sidebar.prototype.removeListeners = function () { + this.sidebar.off('click', '.sidebar-collapsed-icon'); + $('.dropdown').off('hidden.gl.dropdown'); + $('.dropdown').off('loading.gl.dropdown'); + $('.dropdown').off('loaded.gl.dropdown'); + $(document).off('click', '.js-sidebar-toggle'); + } + Sidebar.prototype.addEventListeners = function() { this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked); $('.dropdown').on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden); $('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading); $('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded); - $(document).off('click', '.js-sidebar-toggle').on('click', '.js-sidebar-toggle', function(e, triggered) { + $(document).on('click', '.js-sidebar-toggle', function(e, triggered) { var $allGutterToggleIcons, $this, $thisIcon; e.preventDefault(); $this = $(this); @@ -29,9 +38,7 @@ $('.page-with-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded'); } if (!triggered) { - return $.cookie("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed'), { - path: gon.relative_url_root || '/' - }); + return Cookies.set("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed')); } }); return $(document).off('click', '.js-issuable-todo').on('click', '.js-issuable-todo', this.toggleTodo); diff --git a/app/assets/javascripts/sidebar.js.es6 b/app/assets/javascripts/sidebar.js.es6 index 755fac8107b..73c359af7f6 100644 --- a/app/assets/javascripts/sidebar.js.es6 +++ b/app/assets/javascripts/sidebar.js.es6 @@ -28,7 +28,7 @@ } init() { - this.isPinned = $.cookie(pinnedStateCookie) === 'true'; + this.isPinned = Cookies.get(pinnedStateCookie) === 'true'; this.isExpanded = ( window.innerWidth >= sidebarBreakpoint && $(pageSelector).hasClass(expandedPageClass) @@ -62,10 +62,7 @@ if (!this.isPinned) { this.isExpanded = false; } - $.cookie(pinnedStateCookie, this.isPinned ? 'true' : 'false', { - path: gon.relative_url_root || '/', - expires: 3650 - }); + Cookies.set(pinnedStateCookie, this.isPinned ? 'true' : 'false', { expires: 3650 }); this.renderState(); } diff --git a/app/assets/javascripts/subscription.js b/app/assets/javascripts/subscription.js index 5e3c5983d75..bfa3663bca3 100644 --- a/app/assets/javascripts/subscription.js +++ b/app/assets/javascripts/subscription.js @@ -5,10 +5,10 @@ function Subscription(container) { this.toggleSubscription = bind(this.toggleSubscription, this); var $container; - $container = $(container); - this.url = $container.attr('data-url'); - this.subscribe_button = $container.find('.js-subscribe-button'); - this.subscription_status = $container.find('.subscription-status'); + this.$container = $(container); + this.url = this.$container.attr('data-url'); + this.subscribe_button = this.$container.find('.js-subscribe-button'); + this.subscription_status = this.$container.find('.subscription-status'); this.subscribe_button.unbind('click').click(this.toggleSubscription); } @@ -18,17 +18,27 @@ action = btn.find('span').text(); current_status = this.subscription_status.attr('data-status'); btn.addClass('disabled'); + + if ($('html').hasClass('issue-boards-page')) { + this.url = this.$container.attr('data-url'); + } + return $.post(this.url, (function(_this) { return function() { var status; btn.removeClass('disabled'); - status = current_status === 'subscribed' ? 'unsubscribed' : 'subscribed'; - _this.subscription_status.attr('data-status', status); - action = status === 'subscribed' ? 'Unsubscribe' : 'Subscribe'; - btn.find('span').text(action); - _this.subscription_status.find('>div').toggleClass('hidden'); - if (btn.attr('data-original-title')) { - return btn.tooltip('hide').attr('data-original-title', action).tooltip('fixTitle'); + + if ($('html').hasClass('issue-boards-page')) { + Vue.set(gl.issueBoards.BoardsStore.detail.issue, 'subscribed', !gl.issueBoards.BoardsStore.detail.issue.subscribed); + } else { + status = current_status === 'subscribed' ? 'unsubscribed' : 'subscribed'; + _this.subscription_status.attr('data-status', status); + action = status === 'subscribed' ? 'Unsubscribe' : 'Subscribe'; + btn.find('span').text(action); + _this.subscription_status.find('>div').toggleClass('hidden'); + if (btn.attr('data-original-title')) { + return btn.tooltip('hide').attr('data-original-title', action).tooltip('fixTitle'); + } } }; })(this)); diff --git a/app/assets/javascripts/templates/issuable_template_selector.js.es6 b/app/assets/javascripts/templates/issuable_template_selector.js.es6 index bd4e3c3d00d..fa1b79c8415 100644 --- a/app/assets/javascripts/templates/issuable_template_selector.js.es6 +++ b/app/assets/javascripts/templates/issuable_template_selector.js.es6 @@ -32,24 +32,22 @@ this.currentTemplate = currentTemplate; if (err) return; // Error handled by global AJAX error handler this.stopLoadingSpinner(); - this.setInputValueToTemplateContent(true); + this.setInputValueToTemplateContent(); }); return; } - setInputValueToTemplateContent(append) { + setInputValueToTemplateContent() { // `this.requestFileSuccess` sets the value of the description input field - // to the content of the template selected. If `append` is true, the - // template content will be appended to the previous value of the field, - // separated by a blank line if the previous value is non-empty. + // to the content of the template selected. if (this.titleInput.val() === '') { // If the title has not yet been set, focus the title input and // skip focusing the description input by setting `true` as the // `skipFocus` option to `requestFileSuccess`. - this.requestFileSuccess(this.currentTemplate, {skipFocus: true, append}); + this.requestFileSuccess(this.currentTemplate, {skipFocus: true}); this.titleInput.focus(); } else { - this.requestFileSuccess(this.currentTemplate, {skipFocus: false, append}); + this.requestFileSuccess(this.currentTemplate, {skipFocus: false}); } return; } diff --git a/app/assets/javascripts/user.js.es6 b/app/assets/javascripts/user.js.es6 index 0f97924d94e..111ea7b2ec4 100644 --- a/app/assets/javascripts/user.js.es6 +++ b/app/assets/javascripts/user.js.es6 @@ -23,10 +23,7 @@ hideProjectLimitMessage() { $('.hide-project-limit-message').on('click', e => { e.preventDefault(); - const path = gon.relative_url_root || '/'; - $.cookie('hide_project_limit_message', 'false', { - path: path - }); + Cookies.set('hide_project_limit_message', 'false'); $(this).parents('.project-limit-message').remove(); }); } diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index 3020b7cc239..da6a59bcf97 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -9,7 +9,11 @@ this.usersPath = "/autocomplete/users.json"; this.userPath = "/autocomplete/users/:id.json"; if (currentUser != null) { - this.currentUser = JSON.parse(currentUser); + if (typeof currentUser === 'object') { + this.currentUser = currentUser; + } else { + this.currentUser = JSON.parse(currentUser); + } } $('.js-user-search').each((function(_this) { return function(i, dropdown) { @@ -32,9 +36,30 @@ $value = $block.find('.value'); $collapsedSidebar = $block.find('.sidebar-collapsed-user'); $loading = $block.find('.block-loading').fadeOut(); + + var updateIssueBoardsIssue = function () { + $loading.fadeIn(); + gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update')) + .then(function () { + $loading.fadeOut(); + }); + }; + $block.on('click', '.js-assign-yourself', function(e) { e.preventDefault(); - return assignTo(_this.currentUser.id); + + if ($dropdown.hasClass('js-issue-board-sidebar')) { + Vue.set(gl.issueBoards.BoardsStore.detail.issue, 'assignee', new ListUser({ + id: _this.currentUser.id, + username: _this.currentUser.username, + name: _this.currentUser.name, + avatar_url: _this.currentUser.avatar_url + })); + + updateIssueBoardsIssue(); + } else { + return assignTo(_this.currentUser.id); + } }); assignTo = function(selected) { var data; @@ -150,6 +175,7 @@ // display:block overrides the hide-collapse rule return $value.css('display', ''); }, + vue: $dropdown.hasClass('js-issue-board-sidebar'), clicked: function(user, $el, e) { var isIssueIndex, isMRIndex, page, selected; page = $('body').data('page'); @@ -160,7 +186,7 @@ selectedId = user.id; return; } - if ($('html').hasClass('issue-boards-page')) { + if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) { selectedId = user.id; gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = user.id; gl.issueBoards.BoardsStore.updateFiltersUrl(); @@ -170,6 +196,19 @@ return Issuable.filterResults($dropdown.closest('form')); } else if ($dropdown.hasClass('js-filter-submit')) { return $dropdown.closest('form').submit(); + } else if ($dropdown.hasClass('js-issue-board-sidebar')) { + if (user.id) { + Vue.set(gl.issueBoards.BoardsStore.detail.issue, 'assignee', new ListUser({ + id: user.id, + username: user.username, + name: user.name, + avatar_url: user.avatar_url + })); + } else { + Vue.delete(gl.issueBoards.BoardsStore.detail.issue, 'assignee'); + } + + updateIssueBoardsIssue(); } else { selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('field-name')) + "']").val(); return assignTo(selected); diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss index 1e9a45c19b8..f1d36efb3de 100644 --- a/app/assets/stylesheets/framework/animations.scss +++ b/app/assets/stylesheets/framework/animations.scss @@ -1,10 +1,10 @@ // This file is based off animate.css 3.5.1, available here: // https://github.com/daneden/animate.css/blob/3.5.1/animate.css -// +// // animate.css - http://daneden.me/animate // Version - 3.5.1 // Licensed under the MIT license - http://opensource.org/licenses/MIT -// +// // Copyright (c) 2016 Daniel Eden .animated { @@ -37,7 +37,8 @@ } @include keyframes(pulse) { - from, to { + from, + to { @include webkit-prefix(transform, scale3d(1, 1, 1)); } diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index df2e2ea8d2c..7e168092522 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -128,7 +128,8 @@ position: relative; .avatar-holder { - .avatar, .identicon { + .avatar, + .identicon { margin: 0 auto; float: none; } diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index e6656c2d69a..c0e9c8bf829 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -213,7 +213,8 @@ top: 2px; } - svg, .fa { + svg, + .fa { &:not(:last-child) { margin-right: 3px; } diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 81e4e264560..ad5ac589d0f 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -143,7 +143,8 @@ li.note { } } -.wiki_content code, .readme code { +.wiki_content code, +.readme code { background-color: inherit; } @@ -350,7 +351,8 @@ table { margin-right: 10px; } -.alert, .progress { +.alert, +.progress { margin-bottom: $gl-padding; } @@ -372,3 +374,5 @@ table { margin-right: -$gl-padding; border-top: 1px solid $border-color; } + +.hide-bottom-border { border-bottom: none !important; } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index a839371a6f2..1de246600fd 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -275,7 +275,8 @@ a { padding-left: 25px; - &.is-indeterminate, &.is-active { + &.is-indeterminate, + &.is-active { &::before { position: absolute; left: 5px; @@ -373,7 +374,8 @@ } } -.dropdown-input-field, .default-dropdown-input { +.dropdown-input-field, +.default-dropdown-input { width: 100%; min-height: 30px; padding: 0 7px; @@ -402,7 +404,7 @@ .dropdown-content { max-height: 215px; - overflow-y: scroll; + overflow-y: auto; } .dropdown-footer { diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss index a55dcf4a699..a9006de6d3e 100644 --- a/app/assets/stylesheets/framework/flash.scss +++ b/app/assets/stylesheets/framework/flash.scss @@ -18,7 +18,8 @@ margin: 0; } - .flash-notice, .flash-alert { + .flash-notice, + .flash-alert { border-radius: $border-radius-default; .container-fluid, @@ -30,7 +31,8 @@ &.flash-container-page { margin-bottom: 0; - .flash-notice, .flash-alert { + .flash-notice, + .flash-alert { border-radius: 0; } } diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss index fe834f4e2f6..3f877d86a26 100644 --- a/app/assets/stylesheets/framework/gitlab-theme.scss +++ b/app/assets/stylesheets/framework/gitlab-theme.scss @@ -25,7 +25,9 @@ a { color: $color-light; - &:hover, &:focus, &:active { + &:hover, + &:focus, + &:active { background: $color-dark; } diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 3a4fdd0da22..53ee1ed309e 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -15,7 +15,8 @@ header { margin: 8px 0; text-align: center; - .tanuki-logo, img { + .tanuki-logo, + img { height: 36px; } } @@ -54,7 +55,9 @@ header { line-height: 28px; text-align: center; - &:hover, &:focus, &:active { + &:hover, + &:focus, + &:active { background-color: $background-color; } @@ -125,7 +128,8 @@ header { left: -50%; } - svg, img { + svg, + img { height: 36px; } @@ -222,7 +226,8 @@ header { margin: 0; float: none !important; - .visible-xs, .visable-sm { + .visible-xs, + .visible-sm { display: table-cell !important; } } diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index 4b2627c1b87..48e34a0066e 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -76,14 +76,16 @@ /** light list with border-bottom between li **/ -ul.bordered-list, ul.unstyled-list { +ul.bordered-list, +ul.unstyled-list { @include basic-list; &.top-list { li:first-child { padding-top: 0; - h4, h5 { + h4, + h5 { margin-top: 0; } } diff --git a/app/assets/stylesheets/framework/logo.scss b/app/assets/stylesheets/framework/logo.scss index a90e45bb5f4..429cfbe7235 100644 --- a/app/assets/stylesheets/framework/logo.scss +++ b/app/assets/stylesheets/framework/logo.scss @@ -61,7 +61,7 @@ 10%, 80% { fill: $tanuki-red; } - + 20%, 90% { fill: lighten($tanuki-red, 25%); } diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index 9fe390eb09d..c1ed43bc20f 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -79,7 +79,8 @@ padding-left: 15px !important; } - .nav-links, .nav-links { + .nav-links, + .nav-links { li a { font-size: 14px; padding: 19px 10px; @@ -99,18 +100,21 @@ @media (max-width: $screen-sm-max) { .issues-filters { - .milestone-filter, .labels-filter { + .milestone-filter, + .labels-filter { display: none; } } .page-title { - .note-created-ago, .new-issue-link { + .note-created-ago, + .new-issue-link { display: none; } } - .issue_edited_ago, .note_edited_ago { + .issue_edited_ago, + .note_edited_ago { display: none; } diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss index 8374f30d0b2..8cd49280e1c 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -3,7 +3,7 @@ padding: 15px; .form-actions { - margin: -$gl-padding+1; + margin: -$gl-padding + 1; margin-top: 15px; } diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 899db045b74..fcaf5e18633 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -54,7 +54,9 @@ color: #959494; border-bottom: 2px solid transparent; - &:hover, &:active, &:focus { + &:hover, + &:active, + &:focus { text-decoration: none; outline: none; } @@ -211,7 +213,11 @@ padding-bottom: 0; width: 100%; - .btn, form, .dropdown, .dropdown-menu-toggle, .form-control { + .btn, + form, + .dropdown, + .dropdown-menu-toggle, + .form-control { margin: 0 0 10px; display: block; width: 100%; @@ -245,7 +251,8 @@ } &.adjust { - .nav-text, .nav-controls { + .nav-text, + .nav-controls { width: auto; } } @@ -309,13 +316,15 @@ padding-top: 10px; } - a, i { + a, + i { color: $layout-link-gray; } &.active { - a, i { + a, + i { color: $black; } @@ -328,7 +337,8 @@ } &:hover { - a, i { + a, + i { color: $black; } } diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index e0708c65695..ecdf0be1a05 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -3,7 +3,8 @@ width: 100% !important; } -.select2-container, .select2-container.select2-drop-above { +.select2-container, +.select2-container.select2-drop-above { .select2-choice { background: #fff; border-color: $input-border; @@ -71,7 +72,8 @@ } .select2-container-active { - .select2-choice, .select2-choices { + .select2-choice, + .select2-choices { box-shadow: none; } } diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss index b42075c98d0..9a90d3794fd 100644 --- a/app/assets/stylesheets/framework/tables.scss +++ b/app/assets/stylesheets/framework/tables.scss @@ -23,7 +23,8 @@ table { } tr { - td, th { + td, + th { padding: 10px $gl-padding; line-height: 20px; vertical-align: middle; diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss index f4106641269..59f4594bb83 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap.scss @@ -126,7 +126,8 @@ box-shadow: none; .panel-body { - form, pre { + form, + pre { margin: 0; } diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss index 915aa631ef8..44fe37d3a4a 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss @@ -16,21 +16,21 @@ // $gray-light: lighten($gray-base, 46.7%) // #777 // $gray-lighter: lighten($gray-base, 93.5%) // #eee -$brand-primary: $gl-primary; -$brand-success: $gl-success; -$brand-info: $gl-info; -$brand-warning: $gl-warning; -$brand-danger: $gl-danger; +$brand-primary: $gl-primary; +$brand-success: $gl-success; +$brand-info: $gl-info; +$brand-warning: $gl-warning; +$brand-danger: $gl-danger; -$border-radius-base: 3px !default; -$border-radius-large: 3px !default; -$border-radius-small: 3px !default; +$border-radius-base: 3px !default; +$border-radius-large: 3px !default; +$border-radius-small: 3px !default; //== Scaffolding // -$text-color: $gl-text-color; -$link-color: $gl-link-color; +$text-color: $gl-text-color; +$link-color: $gl-link-color; //== Typography @@ -38,112 +38,112 @@ $link-color: $gl-link-color; //## Font, line-height, and color for body text, headings, and more. $font-family-sans-serif: $regular_font; -$font-family-monospace: $monospace_font; -$font-size-base: $gl-font-size; +$font-family-monospace: $monospace_font; +$font-size-base: $gl-font-size; //== Components // //## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start). -$padding-base-vertical: $gl-vert-padding; -$padding-base-horizontal: $gl-padding; -$component-active-color: #fff; -$component-active-bg: $brand-info; +$padding-base-vertical: $gl-vert-padding; +$padding-base-horizontal: $gl-padding; +$component-active-color: #fff; +$component-active-bg: $brand-info; //== Forms // //## -$input-color: $text-color; -$input-border: $border-color; -$input-border-focus: $focus-border-color; -$legend-color: $text-color; +$input-color: $text-color; +$input-border: $border-color; +$input-border-focus: $focus-border-color; +$legend-color: $text-color; //== Pagination // //## -$pagination-color: $gl-gray; -$pagination-bg: #fff; -$pagination-border: $border-color; +$pagination-color: $gl-gray; +$pagination-bg: #fff; +$pagination-border: $border-color; -$pagination-hover-color: $gl-gray; -$pagination-hover-bg: $row-hover; -$pagination-hover-border: $border-color; +$pagination-hover-color: $gl-gray; +$pagination-hover-bg: $row-hover; +$pagination-hover-border: $border-color; -$pagination-active-color: $blue-dark; -$pagination-active-bg: #fff; -$pagination-active-border: $border-color; +$pagination-active-color: $blue-dark; +$pagination-active-bg: #fff; +$pagination-active-border: $border-color; -$pagination-disabled-color: #cdcdcd; -$pagination-disabled-bg: $background-color; -$pagination-disabled-border: $border-color; +$pagination-disabled-color: #cdcdcd; +$pagination-disabled-bg: $background-color; +$pagination-disabled-border: $border-color; //== Form states and alerts // //## Define colors for form feedback states and, by default, alerts. -$state-success-text: #fff; -$state-success-bg: $brand-success; -$state-success-border: $brand-success; +$state-success-text: #fff; +$state-success-bg: $brand-success; +$state-success-border: $brand-success; -$state-info-text: #fff; -$state-info-bg: $brand-info; -$state-info-border: $brand-info; +$state-info-text: #fff; +$state-info-bg: $brand-info; +$state-info-border: $brand-info; -$state-warning-text: #fff; -$state-warning-bg: $brand-warning; -$state-warning-border: $brand-warning; +$state-warning-text: #fff; +$state-warning-bg: $brand-warning; +$state-warning-border: $brand-warning; -$state-danger-text: #fff; -$state-danger-bg: $brand-danger; -$state-danger-border: $brand-danger; +$state-danger-text: #fff; +$state-danger-bg: $brand-danger; +$state-danger-border: $brand-danger; //== Alerts // //## Define alert colors, border radius, and padding. -$alert-border-radius: 0; +$alert-border-radius: 0; //== Panels // //## -$panel-border-radius: 2px; -$panel-default-text: $text-color; -$panel-default-border: $border-color; +$panel-border-radius: 2px; +$panel-default-text: $text-color; +$panel-default-border: $border-color; $panel-default-heading-bg: $background-color; -$panel-footer-bg: $background-color; -$panel-inner-border: $border-color; +$panel-footer-bg: $background-color; +$panel-inner-border: $border-color; //== Wells // //## -$well-bg: $gray-light; -$well-border: #eee; +$well-bg: $gray-light; +$well-border: #eee; //== Code // //## -$code-color: #c7254e; -$code-bg: #f9f2f4; +$code-color: #c7254e; +$code-bg: #f9f2f4; -$kbd-color: #fff; -$kbd-bg: #333; +$kbd-color: #fff; +$kbd-bg: #333; //== Buttons // //## -$btn-default-color: $gl-text-color; -$btn-default-bg: #fff; -$btn-default-border: #e7e9ed; +$btn-default-color: $gl-text-color; +$btn-default-bg: #fff; +$btn-default-border: #e7e9ed; //== Nav // @@ -153,8 +153,8 @@ $nav-link-padding: 13px $gl-padding; //== Code // //## -$pre-bg: $background-color !default; -$pre-color: $gl-gray !default; +$pre-bg: $background-color !default; +$pre-color: $gl-gray !default; $pre-border-color: $border-color; $table-bg-accent: $background-color; diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 55de9053be5..266a8024809 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -131,12 +131,14 @@ font-weight: inherit; } - ul, ol { + ul, + ol { padding: 0; margin: 3px 0 3px 28px !important; } - ul:dir(rtl), ol:dir(rtl) { + ul:dir(rtl), + ol:dir(rtl) { margin: 3px 28px 3px 0 !important; } @@ -144,7 +146,8 @@ line-height: 1.6em; } - a[href*="/uploads/"], a[href*="storage.googleapis.com/google-code-attachments/"] { + a[href*="/uploads/"], + a[href*="storage.googleapis.com/google-code-attachments/"] { &:before { margin-right: 4px; @@ -167,7 +170,12 @@ } /* Link to current header. */ - h1, h2, h3, h4, h5, h6 { + h1, + h2, + h3, + h4, + h5, + h6 { position: relative; a.anchor { @@ -215,7 +223,12 @@ body { margin: 12px 7px; } -h1, h2, h3, h4, h5, h6 { +h1, +h2, +h3, +h4, +h5, +h6 { color: $gl-title-color; font-weight: 600; } @@ -273,7 +286,10 @@ a > code { text-decoration: line-through; } -h1, h2, h3, h4 { +h1, +h2, +h3, +h4 { small { color: $gl-gray; } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index eafe84570a8..b271f8cf332 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -84,39 +84,39 @@ $warning-message-border: #f0e2bb; /* * UI elements */ -$border-color: #e5e5e5; -$focus-border-color: #3aabf0; -$table-border-color: #f0f0f0; -$background-color: $gray-light; +$border-color: #e5e5e5; +$focus-border-color: #3aabf0; +$table-border-color: #f0f0f0; +$background-color: $gray-light; $dark-background-color: #f5f5f5; -$table-text-gray: #8f8f8f; +$table-text-gray: #8f8f8f; /* * Text */ -$gl-font-size: 15px; -$gl-title-color: #333; -$gl-text-color: #5c5c5c; -$gl-text-color-light: #8c8c8c; -$gl-text-green: #4a2; -$gl-text-red: #d12f19; -$gl-text-orange: #d90; -$gl-link-color: #3084bb; -$gl-dark-link-color: #333; +$gl-font-size: 15px; +$gl-title-color: #333; +$gl-text-color: #5c5c5c; +$gl-text-color-light: #8c8c8c; +$gl-text-green: #4a2; +$gl-text-red: #d12f19; +$gl-text-orange: #d90; +$gl-link-color: #3084bb; +$gl-dark-link-color: #333; $gl-placeholder-color: #8f8f8f; -$gl-icon-color: $gl-placeholder-color; -$gl-grayish-blue: #7f8fa4; -$gl-gray: $gl-text-color; -$gl-gray-dark: #313236; -$gl-gray-light: $gl-placeholder-color; -$gl-header-color: #4c4e54; +$gl-icon-color: $gl-placeholder-color; +$gl-grayish-blue: #7f8fa4; +$gl-gray: $gl-text-color; +$gl-gray-dark: #313236; +$gl-gray-light: $gl-placeholder-color; +$gl-header-color: #4c4e54; /* * Lists */ -$list-font-size: $gl-font-size; +$list-font-size: $gl-font-size; $list-title-color: $gl-title-color; -$list-text-color: $gl-text-color; +$list-text-color: $gl-text-color; $list-text-height: 42px; /* diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss index a3acee299e3..d22d9b01495 100644 --- a/app/assets/stylesheets/highlight/dark.scss +++ b/app/assets/stylesheets/highlight/dark.scss @@ -1,20 +1,25 @@ /* https://github.com/MozMorris/tomorrow-pygments */ .code.dark { // Line numbers - .line-numbers, .diff-line-num { + .line-numbers, + .diff-line-num { background-color: #1d1f21; } - .diff-line-num, .diff-line-num a { + .diff-line-num, + .diff-line-num a { color: rgba(255, 255, 255, 0.3); } // Code itself - pre.code, .diff-line-num { + pre.code, + .diff-line-num { border-color: #666; } - &, pre.code, .line_holder .line_content { + &, + pre.code, + .line_holder .line_content { background-color: #1d1f21; color: #c5c8c6; } @@ -31,11 +36,13 @@ border-color: darken(#557, 15%); } - .diff-line-num.new, .line_content.new { + .diff-line-num.new, + .line_content.new { @include diff_background(rgba(51, 255, 51, 0.1), rgba(51, 255, 51, 0.2), #808080); } - .diff-line-num.old, .line_content.old { + .diff-line-num.old, + .line_content.old { @include diff_background(rgba(255, 51, 51, 0.2), rgba(255, 51, 51, 0.25), #808080); } diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss index e9228c94db9..db8da8aab10 100644 --- a/app/assets/stylesheets/highlight/monokai.scss +++ b/app/assets/stylesheets/highlight/monokai.scss @@ -1,20 +1,25 @@ /* https://github.com/richleland/pygments-css/blob/master/monokai.css */ .code.monokai { // Line numbers - .line-numbers, .diff-line-num { + .line-numbers, + .diff-line-num { background-color: #272822; } - .diff-line-num, .diff-line-num a { + .diff-line-num, + .diff-line-num a { color: rgba(255, 255, 255, 0.3); } // Code itself - pre.code, .diff-line-num { + pre.code, + .diff-line-num { border-color: #555; } - &, pre.code, .line_holder .line_content { + &, + pre.code, + .line_holder .line_content { background-color: #272822; color: #f8f8f2; } @@ -31,11 +36,13 @@ border-color: darken(#49483e, 15%); } - .diff-line-num.new, .line_content.new { + .diff-line-num.new, + .line_content.new { @include diff_background(rgba(166, 226, 46, 0.1), rgba(166, 226, 46, 0.15), #808080); } - .diff-line-num.old, .line_content.old { + .diff-line-num.old, + .line_content.old { @include diff_background(rgba(254, 147, 140, 0.15), rgba(254, 147, 140, 0.2), #808080); } diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss index c3c7773b9e2..a87333146de 100644 --- a/app/assets/stylesheets/highlight/solarized_dark.scss +++ b/app/assets/stylesheets/highlight/solarized_dark.scss @@ -1,20 +1,25 @@ /* https://gist.github.com/qguv/7936275 */ .code.solarized-dark { // Line numbers - .line-numbers, .diff-line-num { + .line-numbers, + .diff-line-num { background-color: #002b36; } - .diff-line-num, .diff-line-num a { + .diff-line-num, + .diff-line-num a { color: rgba(255, 255, 255, 0.3); } // Code itself - pre.code, .diff-line-num { + pre.code, + .diff-line-num { border-color: #113b46; } - &, pre.code, .line_holder .line_content { + &, + pre.code, + .line_holder .line_content { background-color: #002b36; color: #93a1a1; } @@ -31,11 +36,13 @@ border-color: darken(#174652, 15%); } - .diff-line-num.new, .line_content.new { + .diff-line-num.new, + .line_content.new { @include diff_background(rgba(133, 153, 0, 0.15), rgba(133, 153, 0, 0.25), #113b46); } - .diff-line-num.old, .line_content.old { + .diff-line-num.old, + .line_content.old { @include diff_background(rgba(220, 50, 47, 0.3), rgba(220, 50, 47, 0.25), #113b46); } diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss index 5956a28cafe..faff353ded7 100644 --- a/app/assets/stylesheets/highlight/solarized_light.scss +++ b/app/assets/stylesheets/highlight/solarized_light.scss @@ -7,20 +7,25 @@ .code.solarized-light { // Line numbers - .line-numbers, .diff-line-num { + .line-numbers, + .diff-line-num { background-color: #fdf6e3; } - .diff-line-num, .diff-line-num a { + .diff-line-num, + .diff-line-num a { color: $black-transparent; } // Code itself - pre.code, .diff-line-num { + pre.code, + .diff-line-num { border-color: #c5d0d4; } - &, pre.code, .line_holder .line_content { + &, + pre.code, + .line_holder .line_content { background-color: #fdf6e3; color: #586e75; } @@ -37,11 +42,13 @@ border-color: darken(#ddd8c5, 15%); } - .diff-line-num.new, .line_content.new { + .diff-line-num.new, + .line_content.new { @include diff_background(rgba(133, 153, 0, 0.2), rgba(133, 153, 0, 0.25), #c5d0d4); } - .diff-line-num.old, .line_content.old { + .diff-line-num.old, + .line_content.old { @include diff_background(rgba(220, 50, 47, 0.2), rgba(220, 50, 47, 0.25), #c5d0d4); } diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss index 6f31a5235c0..d5367d5f3f0 100644 --- a/app/assets/stylesheets/highlight/white.scss +++ b/app/assets/stylesheets/highlight/white.scss @@ -7,20 +7,25 @@ .code.white { // Line numbers - .line-numbers, .diff-line-num { + .line-numbers, + .diff-line-num { background-color: $background-color; } - .diff-line-num, .diff-line-num a { + .diff-line-num, + .diff-line-num a { color: $black-transparent; } // Code itself - pre.code, .diff-line-num { + pre.code, + .diff-line-num { border-color: $table-border-gray; } - &, pre.code, .line_holder .line_content { + &, + pre.code, + .line_holder .line_content { background-color: #fff; color: #333; } diff --git a/app/assets/stylesheets/mailers/devise.scss b/app/assets/stylesheets/mailers/devise.scss index 9495c5b3f37..b2bce482fde 100644 --- a/app/assets/stylesheets/mailers/devise.scss +++ b/app/assets/stylesheets/mailers/devise.scss @@ -5,13 +5,13 @@ // Styles defined here are embedded directly into the resulting email HTML via // the `premailer` gem. -$body-background-color: #363636; +$body-background-color: #363636; $message-background-color: #fafafa; -$header-color: #6b4fbb; -$body-color: #444; -$cta-color: #e14329; -$footer-link-color: #7e7e7e; +$header-color: #6b4fbb; +$body-color: #444; +$cta-color: #e14329; +$footer-link-color: #7e7e7e; $font-family: Helvetica, Arial, sans-serif; diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss index 140d589024b..63396a6bb29 100644 --- a/app/assets/stylesheets/pages/admin.scss +++ b/app/assets/stylesheets/pages/admin.scss @@ -56,7 +56,8 @@ padding: 10px; text-align: center; - > div, p { + > div, + p { display: inline; margin: 0; diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index d8fabbdcebe..ef6833c9845 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -45,6 +45,15 @@ .page-with-sidebar { padding-bottom: 0; } + + .issues-filters { + position: relative; + z-index: 999999; + } +} + +.boards-app { + position: relative; } .boards-app-loading { @@ -66,6 +75,10 @@ height: 475px; // Needed for PhantomJS height: calc(100vh - 220px); min-height: 475px; + + &.is-compact { + width: calc(100% - 290px); + } } } @@ -184,6 +197,10 @@ margin-bottom: 5px; } + &.is-active { + background-color: $row-hover; + } + .label { border: 0; outline: 0; @@ -212,6 +229,10 @@ margin-right: 5px; font-size: (14px / $issue-boards-font-size) * 1em; } + + .avatar { + margin-left: 0; + } } .card-number { @@ -264,3 +285,48 @@ border-width: 1px 0 1px 1px; } } + +.issue-boards-sidebar { + &.right-sidebar { + top: 153px; + bottom: 0; + + @media (min-width: $screen-sm-min) { + top: 220px; + } + } + + .issuable-sidebar-header { + position: relative; + } + + .gutter-toggle { + position: absolute; + top: 0; + bottom: 15px; + right: 0; + width: 22px; + color: $gray-darkest; + + svg { + position: absolute; + top: 50%; + margin-top: (-11px / 2); + } + + &:hover { + path { + fill: $gray-darkest; + } + } + } + + .issuable-header-text { + width: 100%; + padding-right: 35px; + + > strong { + font-weight: 600; + } + } +} diff --git a/app/assets/stylesheets/pages/ci_projects.scss b/app/assets/stylesheets/pages/ci_projects.scss index 67a9d7d2cf7..87c453a7a27 100644 --- a/app/assets/stylesheets/pages/ci_projects.scss +++ b/app/assets/stylesheets/pages/ci_projects.scss @@ -12,7 +12,8 @@ border-color: $border-color; } - th, td { + th, + td { padding: 10px $gl-padding; } diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss index 264e7e01a34..8ecac08137b 100644 --- a/app/assets/stylesheets/pages/commit.scss +++ b/app/assets/stylesheets/pages/commit.scss @@ -2,14 +2,16 @@ display: block; } -.commit-author, .commit-committer { +.commit-author, +.commit-committer { display: block; color: #999; font-weight: normal; font-style: italic; } -.commit-author strong, .commit-committer strong { +.commit-author strong, +.commit-committer strong { font-weight: bold; font-style: normal; } diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 2b5621e20d6..ad315cfae62 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -63,7 +63,8 @@ display: inline-block; } - .btn-clipboard, .btn-transparent { + .btn-clipboard, + .btn-transparent { padding-left: 0; padding-right: 0; } @@ -162,7 +163,8 @@ .branch-commit { color: $gl-gray; - .commit-id, .commit-row-message { + .commit-id, + .commit-row-message { color: $gl-gray; } } diff --git a/app/assets/stylesheets/pages/confirmation.scss b/app/assets/stylesheets/pages/confirmation.scss index 292225c5261..81e5cee240d 100644 --- a/app/assets/stylesheets/pages/confirmation.scss +++ b/app/assets/stylesheets/pages/confirmation.scss @@ -2,7 +2,12 @@ margin-bottom: 20px; border-bottom: 1px solid #eee; - > h1, h2, h3, h4, h5, h6 { + > h1, + h2, + h3, + h4, + h5, + h6 { font-weight: 400; } @@ -10,7 +15,8 @@ margin-bottom: 20px; } - ul, ol { + ul, + ol { padding-left: 0; } diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index d732008de3d..572e1e7d558 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -9,15 +9,15 @@ padding: 24px 0; border-bottom: none; position: relative; - + @media (max-width: $screen-sm-min) { padding: 6px 0 24px; - } + } } .column { text-align: center; - + @media (max-width: $screen-sm-min) { padding: 15px 0; } @@ -36,7 +36,7 @@ &:last-child { text-align: right; - + @media (max-width: $screen-sm-min) { text-align: center; } @@ -51,7 +51,7 @@ .bordered-box { border: 1px solid $border-color; border-radius: $border-radius-default; - + } .content-list { @@ -73,10 +73,10 @@ font-weight: 600; color: $gl-title-color; } - + &.text { color: $layout-link-gray; - + &.value-col { color: $gl-title-color; } @@ -108,13 +108,13 @@ .svg-container { text-align: center; - + svg { width: 136px; height: 136px; } } - + .inner-content { @media (max-width: $screen-sm-min) { padding: 0 28px; diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss index 2357671c2ae..0f0c0abe7ae 100644 --- a/app/assets/stylesheets/pages/detail_page.scss +++ b/app/assets/stylesheets/pages/detail_page.scss @@ -13,7 +13,8 @@ color: #5c5d5e; } - .issue_created_ago, .author_link { + .issue_created_ago, + .author_link { white-space: nowrap; } } diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index fe6421f8b3f..e0367d1d942 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -124,7 +124,8 @@ } } - .old_line, .new_line { + .old_line, + .new_line { margin: 0; padding: 0; border: none; @@ -222,12 +223,12 @@ top: 13px; right: 7px; } - + .frame { top: 0; right: 0; position: absolute; - + &.deleted { margin: 0; display: block; @@ -281,7 +282,8 @@ position: relative; } - .frame.added, .frame.deleted { + .frame.added, + .frame.deleted { position: absolute; display: block; top: 0; @@ -347,7 +349,8 @@ text-align: center; background: #eee; - ul, li { + ul, + li { list-style: none; margin: 0; padding: 0; diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index 029dabd2138..cb8cefaca97 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -91,7 +91,9 @@ } } - .gitignore-selector, .license-selector, .gitlab-ci-yml-selector { + .gitignore-selector, + .license-selector, + .gitlab-ci-yml-selector { .dropdown { line-height: 21px; } diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 12ee0a5dc3d..fc49ff780fc 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -37,10 +37,10 @@ .branch-name { color: $gl-dark-link-color; } - + .stop-env-link { color: $table-text-gray; - + .stop-env-icon { font-size: 14px; } @@ -48,11 +48,11 @@ .deployment { .build-column { - + .build-link { color: $gl-dark-link-color; } - + .avatar { float: none; } diff --git a/app/assets/stylesheets/pages/errors.scss b/app/assets/stylesheets/pages/errors.scss index 32d2d7b1dbf..11309817d31 100644 --- a/app/assets/stylesheets/pages/errors.scss +++ b/app/assets/stylesheets/pages/errors.scss @@ -2,7 +2,9 @@ max-width: 400px; margin: 0 auto; - h1, h2, h3 { + h1, + h2, + h3 { text-align: center; } diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index 5d9a76dac05..3004959ff7b 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -142,7 +142,7 @@ .event-last-push { overflow: auto; width: 100%; - + .event-last-push-text { @include str-truncated(100%); padding: 4px 0; diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index 623da67a239..3e7fc3fa52c 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -43,7 +43,8 @@ ul.related-merge-requests > li { } } -.merge-requests-title, .related-branches-title { +.merge-requests-title, +.related-branches-title { font-size: 16px; font-weight: 600; } diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss index 9496234c773..3d2b024fe5c 100644 --- a/app/assets/stylesheets/pages/login.scss +++ b/app/assets/stylesheets/pages/login.scss @@ -41,7 +41,8 @@ font-size: 13px; } - .login-box, .omniauth-container { + .login-box, + .omniauth-container { box-shadow: 0 0 0 1px $border-color; border-bottom-right-radius: 2px; border-bottom-left-radius: 2px; @@ -198,7 +199,8 @@ .form-control { - &:active, &:focus { + &:active, + &:focus { background-color: #fff; } } @@ -261,7 +263,8 @@ position: relative; } - .footer-container, hr.footer-fixed { + .footer-container, + hr.footer-fixed { position: absolute; bottom: 0; left: 0; @@ -286,6 +289,13 @@ .new_user { position: relative; padding-bottom: 35px; + + @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { + .forgot-password { + float: none !important; + margin-top: 5px; + } + } } .move-submit-down { diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss index eed2b0ab7cc..032feae8854 100644 --- a/app/assets/stylesheets/pages/merge_conflicts.scss +++ b/app/assets/stylesheets/pages/merge_conflicts.scss @@ -101,7 +101,8 @@ $colors: ( @mixin color-scheme($color) { - .header.line_content, .diff-line-num { + .header.line_content, + .diff-line-num { &.origin { background-color: map-get($colors, #{$color}_header_origin_neutral); border-color: map-get($colors, #{$color}_header_origin_neutral); @@ -254,7 +255,7 @@ $colors: ( border-top: solid 2px $border-green-extra-light; } } - + .editor { pre { height: 350px; diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index dd6d1783667..13402acd8e1 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -50,7 +50,8 @@ } } -.issues-sortable-list, .merge_requests-sortable-list { +.issues-sortable-list, +.merge_requests-sortable-list { .issuable-detail { display: block; margin-top: 7px; diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 17f28959414..16ddef481bd 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -8,7 +8,7 @@ .diff-file .diff-content { tr.line_holder:hover > td .line_note_link { opacity: 1.0; - filter: alpha(opacity=100); + filter: alpha(opacity = 100); } } @@ -24,7 +24,8 @@ display: none; } -.new-note, .note-edit-form { +.new-note, +.note-edit-form { .note-form-actions { margin-top: $gl-padding; } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index fffcdc812a7..b90c91831f2 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -28,7 +28,8 @@ ul.notes { } } - .note-created-ago, .note-updated-at { + .note-created-ago, + .note-updated-at { white-space: nowrap; } @@ -458,7 +459,7 @@ ul.notes { .discussion-next-btn { svg { margin: 0; - + path { fill: $gray-darkest; } diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 5b8dc7f8c40..f88175365c6 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -248,7 +248,8 @@ font-size: 14px; } - svg, .fa { + svg, + .fa { margin-right: 0; } } @@ -529,7 +530,8 @@ // Connect each build (except for first) with curved lines &:not(:first-child) { - &::after, &::before { + &::after, + &::before { content: ''; top: -49px; position: absolute; @@ -555,7 +557,8 @@ // Connect second build to first build with smaller curved line &:nth-child(2) { - &::after, &::before { + &::after, + &::before { height: 29px; top: -9px; } @@ -570,7 +573,8 @@ .build { // Remove right connecting horizontal line from first build in last stage &:first-child { - &::after, &::before { + &::after, + &::before { border: none; } } diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index ed80d2beec2..3f6fdaebc1d 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -253,7 +253,8 @@ } table.u2f-registrations { - th:not(:last-child), td:not(:last-child) { + th:not(:last-child), + td:not(:last-child) { border-right: solid 1px transparent; } }
\ No newline at end of file diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index fe7cf3c87e3..f6355941837 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -6,7 +6,8 @@ } } -.no-ssh-key-message, .project-limit-message { +.no-ssh-key-message, +.project-limit-message { background-color: #f28d35; margin-bottom: 0; } @@ -385,7 +386,8 @@ a.deploy-project-label { text-align: center; width: 169px; - &:hover, &.forked { + &:hover, + &.forked { background-color: $row-hover; border-color: $row-hover-border; } @@ -734,7 +736,8 @@ pre.light-well { .table-bordered { border-radius: 1px; - th:not(:last-child), td:not(:last-child) { + th:not(:last-child), + td:not(:last-child) { border-right: solid 1px transparent; } } @@ -757,7 +760,8 @@ pre.light-well { } } -.project-refs-form .dropdown-menu, .dropdown-menu-projects { +.project-refs-form .dropdown-menu, +.dropdown-menu-projects { width: 300px; @media (min-width: $screen-sm-min) { diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index e77f9816d8a..6d472e8293f 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -65,7 +65,8 @@ .search-input-wrap { width: 100%; - .search-icon, .clear-icon { + .search-icon, + .clear-icon { position: absolute; right: 5px; top: 0; @@ -185,7 +186,8 @@ padding-right: $gl-padding + 15px; } - .btn-search, .btn-new { + .btn-search, + .btn-new { width: 100%; margin-top: 5px; diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss index f1d53c7b8bc..01426e28e92 100644 --- a/app/assets/stylesheets/pages/status.scss +++ b/app/assets/stylesheets/pages/status.scss @@ -74,7 +74,7 @@ .ci-status-icon-success_with_warning { color: $gl-warning; } - + .ci-status-icon-running { color: $blue-normal; } diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 6ea7a2b5498..2b836fa1f4a 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -23,17 +23,18 @@ border-bottom: 1px solid $table-border-gray; border-top: 1px solid $table-border-gray; - td, th { + td, + th { line-height: 21px; } .last-commit { @include str-truncated(506px); - + @media (min-width: $screen-sm-max) and (max-width: $screen-md-max) { @include str-truncated(450px); } - + } .commit-history-link-spacer { @@ -74,7 +75,8 @@ max-width: 320px; vertical-align: middle; - i, a { + i, + a { color: $gl-dark-link-color; } diff --git a/app/assets/stylesheets/print.scss b/app/assets/stylesheets/print.scss index a30b6492572..8239b7e6879 100644 --- a/app/assets/stylesheets/print.scss +++ b/app/assets/stylesheets/print.scss @@ -1,7 +1,24 @@ -.wiki h1, .wiki h2, .wiki h3, .wiki h4, .wiki h5, .wiki h6 {margin-top: 17px; } -.wiki h1 {font-size: 30px;} -.wiki h2 {font-size: 22px;} -.wiki h3 {font-size: 18px; font-weight: bold; } +.wiki h1, +.wiki h2, +.wiki h3, +.wiki h4, +.wiki h5, +.wiki h6 { + margin-top: 17px; +} + +.wiki h1 { + font-size: 30px; +} + +.wiki h2 { + font-size: 22px; +} + +.wiki h3 { + font-size: 18px; + font-weight: bold; +} header, nav, diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index 18cd800c619..940a3ad20ba 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -21,6 +21,10 @@ class Groups::GroupMembersController < Groups::ApplicationController end def create + if params[:user_ids].blank? + return redirect_to(group_group_members_path(@group), alert: 'No users specified.') + end + @group.add_users( params[:user_ids].split(','), params[:access_level], diff --git a/app/controllers/import/gitlab_projects_controller.rb b/app/controllers/import/gitlab_projects_controller.rb index 3ec173abcdb..36d246d185b 100644 --- a/app/controllers/import/gitlab_projects_controller.rb +++ b/app/controllers/import/gitlab_projects_controller.rb @@ -2,8 +2,8 @@ class Import::GitlabProjectsController < Import::BaseController before_action :verify_gitlab_project_import_enabled def new - @namespace_id = project_params[:namespace_id] - @namespace_name = Namespace.find(project_params[:namespace_id]).name + @namespace = Namespace.find(project_params[:namespace_id]) + return render_404 unless current_user.can?(:create_projects, @namespace) @path = project_params[:path] end diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb index a2b01ff43dc..dc33e1405f2 100644 --- a/app/controllers/projects/boards/issues_controller.rb +++ b/app/controllers/projects/boards/issues_controller.rb @@ -73,10 +73,13 @@ module Projects def serialize_as_json(resource) resource.as_json( labels: true, - only: [:iid, :title, :confidential], + only: [:iid, :title, :confidential, :due_date], include: { - assignee: { only: [:id, :name, :username], methods: [:avatar_url] } - }) + assignee: { only: [:id, :name, :username], methods: [:avatar_url] }, + milestone: { only: [:id, :title] } + }, + user: current_user + ) end end end diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index 2a07d154853..d08f490de18 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -25,6 +25,10 @@ class Projects::ProjectMembersController < Projects::ApplicationController end def create + if params[:user_ids].blank? + return redirect_to(namespace_project_project_members_path(@project.namespace, @project), alert: 'No users or groups specified.') + end + @project.team.add_users( params[:user_ids].split(','), params[:access_level], @@ -32,7 +36,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController current_user: current_user ) - redirect_to namespace_project_project_members_path(@project.namespace, @project) + redirect_to namespace_project_project_members_path(@project.namespace, @project), notice: 'Users were successfully added.' end def update diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb index 6ace14a4bb5..44484d64567 100644 --- a/app/finders/labels_finder.rb +++ b/app/finders/labels_finder.rb @@ -35,8 +35,10 @@ class LabelsFinder < UnionFinder end def with_title(items) - items = items.where(title: title) if title - items + return items if title.nil? + return items.none if title.blank? + + items.where(title: title) end def group_id @@ -48,11 +50,11 @@ class LabelsFinder < UnionFinder end def projects_ids - params[:project_ids].presence + params[:project_ids] end def title - params[:title].presence || params[:name].presence + params[:title] || params[:name] end def project diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb index b7247ffa8b2..38c586ccd31 100644 --- a/app/helpers/boards_helper.rb +++ b/app/helpers/boards_helper.rb @@ -5,7 +5,7 @@ module BoardsHelper { endpoint: namespace_project_boards_path(@project.namespace, @project), board_id: board.id, - disabled: !can?(current_user, :admin_list, @project), + disabled: "#{!can?(current_user, :admin_list, @project)}", issue_link_base: namespace_project_issues_path(@project.namespace, @project) } end diff --git a/app/helpers/sidekiq_helper.rb b/app/helpers/sidekiq_helper.rb index d440edc55ba..56749d80bd3 100644 --- a/app/helpers/sidekiq_helper.rb +++ b/app/helpers/sidekiq_helper.rb @@ -5,7 +5,7 @@ module SidekiqHelper (?<mem>[\d\.,]+)\s+ (?<state>[DRSTWXZNLsl\+<]+)\s+ (?<start>.+)\s+ - (?<command>sidekiq.*\])\s+ + (?<command>sidekiq.*\])\s* \z/x def parse_sidekiq_ps(line) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index a6b606d13de..bf5f92f8462 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -3,8 +3,8 @@ module Ci include TokenAuthenticatable include AfterCommitQueue - belongs_to :runner, class_name: 'Ci::Runner' - belongs_to :trigger_request, class_name: 'Ci::TriggerRequest' + belongs_to :runner + belongs_to :trigger_request belongs_to :erased_by, class_name: 'User' serialize :options diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index d5c1e03b461..adda3b8f40c 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -7,12 +7,12 @@ module Ci self.table_name = 'ci_commits' - belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id + belongs_to :project, foreign_key: :gl_project_id belongs_to :user has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id - has_many :builds, class_name: 'Ci::Build', foreign_key: :commit_id - has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest', foreign_key: :commit_id + has_many :builds, foreign_key: :commit_id + has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id validates_presence_of :sha, unless: :importing? validates_presence_of :ref, unless: :importing? diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index 44cb19ece3b..123930273e0 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -6,9 +6,9 @@ module Ci AVAILABLE_SCOPES = %w[specific shared active paused online] FORM_EDITABLE = %i[description tag_list active run_untagged locked] - has_many :builds, class_name: 'Ci::Build' - has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject' - has_many :projects, through: :runner_projects, class_name: '::Project', foreign_key: :gl_project_id + has_many :builds + has_many :runner_projects, dependent: :destroy + has_many :projects, through: :runner_projects, foreign_key: :gl_project_id has_one :last_build, ->() { order('id DESC') }, class_name: 'Ci::Build' diff --git a/app/models/ci/runner_project.rb b/app/models/ci/runner_project.rb index 4b44ffa886e..1f9baeca5b1 100644 --- a/app/models/ci/runner_project.rb +++ b/app/models/ci/runner_project.rb @@ -2,8 +2,8 @@ module Ci class RunnerProject < ActiveRecord::Base extend Ci::Model - belongs_to :runner, class_name: 'Ci::Runner' - belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id + belongs_to :runner + belongs_to :project, foreign_key: :gl_project_id validates_uniqueness_of :runner_id, scope: :gl_project_id end diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb index a0b19b51a12..62889fe80d8 100644 --- a/app/models/ci/trigger.rb +++ b/app/models/ci/trigger.rb @@ -4,8 +4,8 @@ module Ci acts_as_paranoid - belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id - has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest' + belongs_to :project, foreign_key: :gl_project_id + has_many :trigger_requests, dependent: :destroy validates_presence_of :token validates_uniqueness_of :token diff --git a/app/models/ci/trigger_request.rb b/app/models/ci/trigger_request.rb index fc674871743..2b807731d0d 100644 --- a/app/models/ci/trigger_request.rb +++ b/app/models/ci/trigger_request.rb @@ -2,9 +2,9 @@ module Ci class TriggerRequest < ActiveRecord::Base extend Ci::Model - belongs_to :trigger, class_name: 'Ci::Trigger' - belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id - has_many :builds, class_name: 'Ci::Build' + belongs_to :trigger + belongs_to :pipeline, foreign_key: :commit_id + has_many :builds serialize :variables diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb index 6959223aed9..94d9e2b3208 100644 --- a/app/models/ci/variable.rb +++ b/app/models/ci/variable.rb @@ -2,7 +2,7 @@ module Ci class Variable < ActiveRecord::Base extend Ci::Model - belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id + belongs_to :project, foreign_key: :gl_project_id validates_uniqueness_of :key, scope: :gl_project_id validates :key, diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 7b554be4f9a..4cb3a69416e 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -5,7 +5,7 @@ class CommitStatus < ActiveRecord::Base self.table_name = 'ci_builds' - belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id + belongs_to :project, foreign_key: :gl_project_id belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id belongs_to :user diff --git a/app/models/concerns/protected_branch_access.rb b/app/models/concerns/protected_branch_access.rb index 5a7b36070e7..7fd0905ee81 100644 --- a/app/models/concerns/protected_branch_access.rb +++ b/app/models/concerns/protected_branch_access.rb @@ -1,6 +1,11 @@ module ProtectedBranchAccess extend ActiveSupport::Concern + included do + scope :master, -> { where(access_level: Gitlab::Access::MASTER) } + scope :developer, -> { where(access_level: Gitlab::Access::DEVELOPER) } + end + def humanize self.class.human_access_levels[self.access_level] end diff --git a/app/models/email.rb b/app/models/email.rb index 32a412ab878..826d4f16edb 100644 --- a/app/models/email.rb +++ b/app/models/email.rb @@ -7,10 +7,8 @@ class Email < ActiveRecord::Base validates :email, presence: true, uniqueness: true, email: true validate :unique_email, if: ->(email) { email.email_changed? } - before_validation :cleanup_email - - def cleanup_email - self.email = self.email.downcase.strip + def email=(value) + write_attribute(:email, value.downcase.strip) end def unique_email diff --git a/app/models/group.rb b/app/models/group.rb index 6865e610718..552e1154df6 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -6,7 +6,7 @@ class Group < Namespace include AccessRequestable include Referable - has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'GroupMember' + has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source alias_method :members, :group_members has_many :users, through: :group_members has_many :owners, @@ -68,7 +68,7 @@ class Group < Namespace end def web_url - Gitlab::Routing.url_helpers.group_canonical_url(self) + Gitlab::Routing.url_helpers.group_url(self) end def human_name diff --git a/app/models/issue.rb b/app/models/issue.rb index 89158a50353..e356fe06363 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -287,10 +287,12 @@ class Issue < ActiveRecord::Base def as_json(options = {}) super(options).tap do |json| + json[:subscribed] = subscribed?(options[:user]) if options.has_key?(:user) + if options.has_key?(:labels) json[:labels] = labels.as_json( project: project, - only: [:id, :title, :description, :color], + only: [:id, :title, :description, :color, :priority], methods: [:text_color] ) end diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index 1b54a85d064..204f34f0269 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -1,7 +1,7 @@ class GroupMember < Member SOURCE_TYPE = 'Namespace' - belongs_to :group, class_name: 'Group', foreign_key: 'source_id' + belongs_to :group, foreign_key: 'source_id' # Make sure group member points only to group as it source default_value_for :source_type, SOURCE_TYPE diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index e4880973117..008fff0857c 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -3,7 +3,7 @@ class ProjectMember < Member include Gitlab::ShellAdapter - belongs_to :project, class_name: 'Project', foreign_key: 'source_id' + belongs_to :project, foreign_key: 'source_id' # Make sure project member points only to project as it source default_value_for :source_type, SOURCE_TYPE diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index c476a3bb14e..4872f8b8649 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -6,8 +6,8 @@ class MergeRequest < ActiveRecord::Base include Taskable include Importable - belongs_to :target_project, foreign_key: :target_project_id, class_name: "Project" - belongs_to :source_project, foreign_key: :source_project_id, class_name: "Project" + belongs_to :target_project, class_name: "Project" + belongs_to :source_project, class_name: "Project" belongs_to :merge_user, class_name: "User" has_many :merge_request_diffs, dependent: :destroy diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index b8a10b7968e..dd65a9a8b86 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -299,8 +299,10 @@ class MergeRequestDiff < ActiveRecord::Base end def keep_around_commits - repository.keep_around(start_commit_sha) - repository.keep_around(head_commit_sha) - repository.keep_around(base_commit_sha) + [repository, merge_request.source_project.repository].each do |repo| + repo.keep_around(start_commit_sha) + repo.keep_around(head_commit_sha) + repo.keep_around(base_commit_sha) + end end end diff --git a/app/models/project.rb b/app/models/project.rb index af117f0acb0..fbf7012972e 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -63,11 +63,11 @@ class Project < ActiveRecord::Base alias_attribute :title, :name # Relations - belongs_to :creator, foreign_key: 'creator_id', class_name: 'User' + belongs_to :creator, class_name: 'User' belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'namespace_id' belongs_to :namespace - has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event', foreign_key: 'project_id' + has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event' has_many :boards, before_add: :validate_board_limit, dependent: :destroy # Project services @@ -116,7 +116,7 @@ class Project < ActiveRecord::Base has_many :hooks, dependent: :destroy, class_name: 'ProjectHook' has_many :protected_branches, dependent: :destroy - has_many :project_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'ProjectMember' + has_many :project_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source alias_method :members, :project_members has_many :users, through: :project_members @@ -137,7 +137,7 @@ class Project < ActiveRecord::Base has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" has_one :project_feature, dependent: :destroy - has_many :commit_statuses, dependent: :destroy, class_name: 'CommitStatus', foreign_key: :gl_project_id + has_many :commit_statuses, dependent: :destroy, foreign_key: :gl_project_id has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline', foreign_key: :gl_project_id has_many :builds, class_name: 'Ci::Build', foreign_key: :gl_project_id # the builds are created from the commit_statuses has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject', foreign_key: :gl_project_id diff --git a/app/models/repository.rb b/app/models/repository.rb index 1b7f20a2134..4ae9c20726f 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -419,6 +419,17 @@ class Repository @exists = nil end + # expire cache that doesn't depend on repository data (when expiring) + def expire_content_cache + expire_tags_cache + expire_tag_count_cache + expire_branches_cache + expire_branch_count_cache + expire_root_ref_cache + expire_emptiness_caches + expire_exists_cache + end + # Runs code after a repository has been created. def after_create expire_exists_cache @@ -434,14 +445,7 @@ class Repository expire_cache if exists? - # expire cache that don't depend on repository data (when expiring) - expire_tags_cache - expire_tag_count_cache - expire_branches_cache - expire_branch_count_cache - expire_root_ref_cache - expire_emptiness_caches - expire_exists_cache + expire_content_cache repository_event(:remove_repository) end @@ -473,14 +477,13 @@ class Repository end def before_import - expire_emptiness_caches - expire_exists_cache + expire_content_cache end # Runs code after a repository has been forked/imported. def after_import - expire_emptiness_caches - expire_exists_cache + expire_content_cache + build_cache end # Runs code after a new commit has been pushed. diff --git a/app/models/user.rb b/app/models/user.rb index f367f4616fb..9e76df63d31 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -47,7 +47,7 @@ class User < ActiveRecord::Base # # Namespace for personal projects - has_one :namespace, -> { where type: nil }, dependent: :destroy, foreign_key: :owner_id, class_name: "Namespace" + has_one :namespace, -> { where type: nil }, dependent: :destroy, foreign_key: :owner_id # Profile has_many :keys, dependent: :destroy @@ -66,17 +66,17 @@ class User < ActiveRecord::Base # Projects has_many :groups_projects, through: :groups, source: :projects has_many :personal_projects, through: :namespace, source: :projects - has_many :project_members, -> { where(requested_at: nil) }, dependent: :destroy, class_name: 'ProjectMember' + has_many :project_members, -> { where(requested_at: nil) }, dependent: :destroy has_many :projects, through: :project_members has_many :created_projects, foreign_key: :creator_id, class_name: 'Project' has_many :users_star_projects, dependent: :destroy has_many :starred_projects, through: :users_star_projects, source: :project - has_many :snippets, dependent: :destroy, foreign_key: :author_id, class_name: "Snippet" + has_many :snippets, dependent: :destroy, foreign_key: :author_id has_many :issues, dependent: :destroy, foreign_key: :author_id has_many :notes, dependent: :destroy, foreign_key: :author_id has_many :merge_requests, dependent: :destroy, foreign_key: :author_id - has_many :events, dependent: :destroy, foreign_key: :author_id, class_name: "Event" + has_many :events, dependent: :destroy, foreign_key: :author_id has_many :subscriptions, dependent: :destroy has_many :recent_events, -> { order "id DESC" }, foreign_key: :author_id, class_name: "Event" has_many :assigned_issues, dependent: :destroy, foreign_key: :assignee_id, class_name: "Issue" @@ -309,7 +309,7 @@ class User < ActiveRecord::Base username end - def to_reference(_from_project = nil) + def to_reference(_from_project = nil, _target_project = nil) "#{self.class.reference_prefix}#{username}" end diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb index a36008c3ef5..723cc0e6834 100644 --- a/app/services/notes/create_service.rb +++ b/app/services/notes/create_service.rb @@ -7,8 +7,10 @@ module Notes if note.award_emoji? noteable = note.noteable - todo_service.new_award_emoji(noteable, current_user) - return noteable.create_award_emoji(note.award_emoji_name, current_user) + if noteable.user_can_award?(current_user, note.award_emoji_name) + todo_service.new_award_emoji(noteable, current_user) + return noteable.create_award_emoji(note.award_emoji_name, current_user) + end end # We execute commands (extracted from `params[:note]`) on the noteable diff --git a/app/services/protected_branches/api_create_service.rb b/app/services/protected_branches/api_create_service.rb new file mode 100644 index 00000000000..f2040dfa03a --- /dev/null +++ b/app/services/protected_branches/api_create_service.rb @@ -0,0 +1,29 @@ +# The protected branches API still uses the `developers_can_push` and `developers_can_merge` +# flags for backward compatibility, and so performs translation between that format and the +# internal data model (separate access levels). The translation code is non-trivial, and so +# lives in this service. +module ProtectedBranches + class ApiCreateService < BaseService + def execute + push_access_level = + if params.delete(:developers_can_push) + Gitlab::Access::DEVELOPER + else + Gitlab::Access::MASTER + end + + merge_access_level = + if params.delete(:developers_can_merge) + Gitlab::Access::DEVELOPER + else + Gitlab::Access::MASTER + end + + @params.merge!(push_access_levels_attributes: [{ access_level: push_access_level }], + merge_access_levels_attributes: [{ access_level: merge_access_level }]) + + service = ProtectedBranches::CreateService.new(@project, @current_user, @params) + service.execute + end + end +end diff --git a/app/services/protected_branches/api_update_service.rb b/app/services/protected_branches/api_update_service.rb new file mode 100644 index 00000000000..050cb3b738b --- /dev/null +++ b/app/services/protected_branches/api_update_service.rb @@ -0,0 +1,47 @@ +# The protected branches API still uses the `developers_can_push` and `developers_can_merge` +# flags for backward compatibility, and so performs translation between that format and the +# internal data model (separate access levels). The translation code is non-trivial, and so +# lives in this service. +module ProtectedBranches + class ApiUpdateService < BaseService + def execute(protected_branch) + @developers_can_push = params.delete(:developers_can_push) + @developers_can_merge = params.delete(:developers_can_merge) + + @protected_branch = protected_branch + + protected_branch.transaction do + delete_redundant_access_levels + + case @developers_can_push + when true + params.merge!(push_access_levels_attributes: [{ access_level: Gitlab::Access::DEVELOPER }]) + when false + params.merge!(push_access_levels_attributes: [{ access_level: Gitlab::Access::MASTER }]) + end + + case @developers_can_merge + when true + params.merge!(merge_access_levels_attributes: [{ access_level: Gitlab::Access::DEVELOPER }]) + when false + params.merge!(merge_access_levels_attributes: [{ access_level: Gitlab::Access::MASTER }]) + end + + service = ProtectedBranches::UpdateService.new(@project, @current_user, @params) + service.execute(protected_branch) + end + end + + private + + def delete_redundant_access_levels + unless @developers_can_merge.nil? + @protected_branch.merge_access_levels.destroy_all + end + + unless @developers_can_push.nil? + @protected_branch.push_access_levels.destroy_all + end + end + end +end diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml index 525e7d99d71..5fd896f6835 100644 --- a/app/views/devise/sessions/_new_base.html.haml +++ b/app/views/devise/sessions/_new_base.html.haml @@ -12,5 +12,5 @@ %label{for: "user_remember_me"} = f.check_box :remember_me %span Remember me - .pull-right + .pull-right.forgot-password = link_to "Forgot your password?", new_password_path(resource_name) diff --git a/app/views/devise/shared/_tabs_normal.html.haml b/app/views/devise/shared/_tabs_normal.html.haml index 79b1d447a92..05246303fb6 100644 --- a/app/views/devise/shared/_tabs_normal.html.haml +++ b/app/views/devise/shared/_tabs_normal.html.haml @@ -1,5 +1,6 @@ %ul.nav-links.new-session-tabs.nav-tabs{ role: 'tablist'} %li.active{ role: 'presentation' } %a{ href: '#login-pane', data: { toggle: 'tab' }, role: 'tab'} Sign in - %li{ role: 'presentation'} - %a{ href: '#register-pane', data: { toggle: 'tab' }, role: 'tab'} Register + - if signin_enabled? && signup_enabled? + %li{ role: 'presentation'} + %a{ href: '#register-pane', data: { toggle: 'tab' }, role: 'tab'} Register diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index d16bd61b779..070ed90da6d 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -461,7 +461,7 @@ .panel-body = lorem - %h2#alert Alerts + %h2#alerts Alerts .row .col-md-6 diff --git a/app/views/import/gitlab_projects/new.html.haml b/app/views/import/gitlab_projects/new.html.haml index 44e2653ca4a..767dffb5589 100644 --- a/app/views/import/gitlab_projects/new.html.haml +++ b/app/views/import/gitlab_projects/new.html.haml @@ -9,12 +9,12 @@ %p Project will be imported as %strong - #{@namespace_name}/#{@path} + #{@namespace.name}/#{@path} %p To move or copy an entire GitLab project from another GitLab installation to this one, navigate to the original project's settings page, generate an export file, and upload it here. .form-group - = hidden_field_tag :namespace_id, @namespace_id + = hidden_field_tag :namespace_id, @namespace.id = hidden_field_tag :path, @path = label_tag :file, class: 'control-label' do %span GitLab project export diff --git a/app/views/projects/boards/components/_card.html.haml b/app/views/projects/boards/components/_card.html.haml index d8f16022407..8fce702314c 100644 --- a/app/views/projects/boards/components/_card.html.haml +++ b/app/views/projects/boards/components/_card.html.haml @@ -7,8 +7,11 @@ ":issue-link-base" => "issueLinkBase", ":disabled" => "disabled", "track-by" => "id" } - %li.card{ ":class" => "{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id }", - ":index" => "index" } + %li.card{ ":class" => "{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, 'is-active': issueDetailVisible }", + ":index" => "index", + "@mousedown" => "mouseDown", + "@mouseMove" => "mouseMove", + "@mouseup" => "showIssue($event)" } %h4.card-title = icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential") %a{ ":href" => "issueLinkBase + '/' + issue.id", @@ -18,6 +21,11 @@ %span.card-number{ "v-if" => "issue.id" } = precede '#' do {{ issue.id }} + %a.has-tooltip{ ":href" => "'#{root_path}' + issue.assignee.username", + ":title" => "'Assigned to ' + issue.assignee.name", + "v-if" => "issue.assignee", + data: { container: 'body' } } + %img.avatar.avatar-inline.s20{ ":src" => "issue.assignee.avatar", width: 20, height: 20 } %button.label.color-label.has-tooltip{ "v-for" => "label in issue.labels", type: "button", "v-if" => "(!list.label || label.id !== list.label.id)", @@ -26,8 +34,3 @@ ":title" => "label.description", data: { container: 'body' } } {{ label.title }} - %a.has-tooltip{ ":href" => "'/' + issue.assignee.username", - ":title" => "'Assigned to ' + issue.assignee.name", - "v-if" => "issue.assignee", - data: { container: 'body' } } - %img.avatar.avatar-inline.s20{ ":src" => "issue.assignee.avatar", width: 20, height: 20 } diff --git a/app/views/projects/boards/components/_sidebar.html.haml b/app/views/projects/boards/components/_sidebar.html.haml new file mode 100644 index 00000000000..f0c0c6953e0 --- /dev/null +++ b/app/views/projects/boards/components/_sidebar.html.haml @@ -0,0 +1,23 @@ +%board-sidebar{ "inline-template" => true, + ":current-user" => "#{current_user.to_json(only: [:username, :id, :name], methods: [:avatar_url]) if current_user}" } + %aside.right-sidebar.right-sidebar-expanded.issue-boards-sidebar{ "v-show" => "showSidebar" } + .issuable-sidebar + .block.issuable-sidebar-header + %span.issuable-header-text.hide-collapsed.pull-left + %strong + {{ issue.title }} + %br/ + %span + = precede "#" do + {{ issue.id }} + %a.gutter-toggle.pull-right{ role: "button", + href: "#", + "@click.prevent" => "closeSidebar", + "aria-label" => "Toggle sidebar" } + = custom_icon("icon_close", size: 15) + .js-issuable-update + = render "projects/boards/components/sidebar/assignee" + = render "projects/boards/components/sidebar/milestone" + = render "projects/boards/components/sidebar/due_date" + = render "projects/boards/components/sidebar/labels" + = render "projects/boards/components/sidebar/notifications" diff --git a/app/views/projects/boards/components/sidebar/_assignee.html.haml b/app/views/projects/boards/components/sidebar/_assignee.html.haml new file mode 100644 index 00000000000..604e13858d1 --- /dev/null +++ b/app/views/projects/boards/components/sidebar/_assignee.html.haml @@ -0,0 +1,40 @@ +.block.assignee + .title.hide-collapsed + Assignee + = icon("spinner spin", class: "block-loading") + - if can?(current_user, :admin_issue, @project) + = link_to "Edit", "#", class: "edit-link pull-right" + .value.hide-collapsed + %span.assign-yourself.no-value{ "v-if" => "!issue.assignee" } + No assignee + - if can?(current_user, :admin_issue, @project) + \- + %a.js-assign-yourself{ href: "#" } + assign yourself + %a.author_link.bold{ ":href" => "'#{root_url}' + issue.assignee.username", + "v-if" => "issue.assignee" } + %img.avatar.avatar-inline.s32{ ":src" => "issue.assignee.avatar", + width: "32" } + %span.author + {{ issue.assignee.name }} + %span.username + = precede "@" do + {{ issue.assignee.username }} + - if can?(current_user, :admin_issue, @project) + .selectbox.hide-collapsed + %input{ type: "hidden", + name: "issue[assignee_id]", + id: "issue_assignee_id", + ":value" => "issue.assignee.id", + "v-if" => "issue.assignee" } + .dropdown + %button.dropdown-menu-toggle.js-user-search.js-author-search.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", field_name: "issue[assignee_id]", first_user: (current_user.username if current_user), current_user: "true", project_id: @project.id, null_user: "true" }, + ":data-issuable-id" => "issue.id", + ":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" } + Select assignee + = icon("chevron-down") + .dropdown-menu.dropdown-menu-user.dropdown-menu-selectable.dropdown-menu-author + = dropdown_title("Assign to") + = dropdown_filter("Search users") + = dropdown_content + = dropdown_loading diff --git a/app/views/projects/boards/components/sidebar/_due_date.html.haml b/app/views/projects/boards/components/sidebar/_due_date.html.haml new file mode 100644 index 00000000000..c7da1d0d4ac --- /dev/null +++ b/app/views/projects/boards/components/sidebar/_due_date.html.haml @@ -0,0 +1,32 @@ +.block.due_date + .title + Due date + = icon("spinner spin", class: "block-loading") + - if can?(current_user, :admin_issue, @project) + = link_to "Edit", "#", class: "edit-link pull-right" + .value + .value-content + %span.no-value{ "v-if" => "!issue.dueDate" } + No due date + %span.bold{ "v-if" => "issue.dueDate" } + {{ issue.dueDate | due-date }} + - if can?(current_user, :admin_issue, @project) + %span.no-value.js-remove-due-date-holder{ "v-if" => "issue.dueDate" } + \- + %a.js-remove-due-date{ href: "#", role: "button" } + remove due date + - if can?(current_user, :admin_issue, @project) + .selectbox + %input{ type: "hidden", + name: "issue[due_date]", + ":value" => "issue.dueDate" } + .dropdown + %button.dropdown-menu-toggle.js-due-date-select.js-issue-boards-due-date{ type: 'button', + data: { toggle: 'dropdown', field_name: "issue[due_date]", ability_name: "issue" }, + ":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" } + %span.dropdown-toggle-text Due date + = icon('chevron-down') + .dropdown-menu.dropdown-menu-due-date + = dropdown_title('Due date') + = dropdown_content do + .js-due-date-calendar diff --git a/app/views/projects/boards/components/sidebar/_labels.html.haml b/app/views/projects/boards/components/sidebar/_labels.html.haml new file mode 100644 index 00000000000..ce68e5e1998 --- /dev/null +++ b/app/views/projects/boards/components/sidebar/_labels.html.haml @@ -0,0 +1,30 @@ +.block.labels + .title + Labels + = icon("spinner spin", class: "block-loading") + - if can?(current_user, :admin_issue, @project) + = link_to "Edit", "#", class: "edit-link pull-right" + .value.issuable-show-labels + %span.no-value{ "v-if" => "issue.labels && issue.labels.length === 0" } + None + %a{ href: "#", + "v-for" => "label in issue.labels" } + %span.label.color-label.has-tooltip{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" } + {{ label.title }} + - if can?(current_user, :admin_issue, @project) + .selectbox + %input{ type: "hidden", + name: "issue[label_names][]", + "v-for" => "label in issue.labels", + ":value" => "label.id" } + .dropdown + %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-issue-board-sidebar{ type: "button", + data: { toggle: "dropdown", field_name: "issue[label_names][]", show_no: "true", show_any: "true", project_id: @project.id, labels: namespace_project_labels_path(@project.namespace, @project, :json), namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path) }, + ":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" } + %span.dropdown-toggle-text + Label + = icon('chevron-down') + .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable + = render partial: "shared/issuable/label_page_default" + - if can? current_user, :admin_label, @project and @project + = render partial: "shared/issuable/label_page_create" diff --git a/app/views/projects/boards/components/sidebar/_milestone.html.haml b/app/views/projects/boards/components/sidebar/_milestone.html.haml new file mode 100644 index 00000000000..3cd20d1c0f7 --- /dev/null +++ b/app/views/projects/boards/components/sidebar/_milestone.html.haml @@ -0,0 +1,28 @@ +.block.milestone + .title + Milestone + = icon("spinner spin", class: "block-loading") + - if can?(current_user, :admin_issue, @project) + = link_to "Edit", "#", class: "edit-link pull-right" + .value + %span.no-value{ "v-if" => "!issue.milestone" } + None + %span.bold.has-tooltip{ "v-if" => "issue.milestone" } + {{ issue.milestone.title }} + - if can?(current_user, :admin_issue, @project) + .selectbox + %input{ type: "hidden", + ":value" => "issue.milestone.id", + name: "issue[milestone_id]", + "v-if" => "issue.milestone" } + .dropdown + %button.dropdown-menu-toggle.js-milestone-select.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", show_no: "true", field_name: "issue[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: "issue", use_id: "true" }, + ":data-issuable-id" => "issue.id", + ":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" } + Milestone + = icon("chevron-down") + .dropdown-menu.dropdown-select.dropdown-menu-selectable + = dropdown_title("Assignee milestone") + = dropdown_filter("Search milestones") + = dropdown_content + = dropdown_loading diff --git a/app/views/projects/boards/components/sidebar/_notifications.html.haml b/app/views/projects/boards/components/sidebar/_notifications.html.haml new file mode 100644 index 00000000000..21c9563e9db --- /dev/null +++ b/app/views/projects/boards/components/sidebar/_notifications.html.haml @@ -0,0 +1,11 @@ +- if current_user + .block.light.subscription{ ":data-url" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '/toggle_subscription'" } + .title + Notifications + %button.btn.btn-block.btn-default.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" } + {{ issue.subscribed ? 'Unsubscribe' : 'Subscribe' }} + .subscription-status{ ":data-status" => "issue.subscribed ? 'subscribed' : 'unsubscribed'" } + .unsubscribed{ "v-show" => "!issue.subscribed" } + You're not receiving notifications from this thread. + .subscribed{ "v-show" => "issue.subscribed" } + You're receiving notifications because you're subscribed to this thread. diff --git a/app/views/projects/boards/index.html.haml b/app/views/projects/boards/index.html.haml index 885f8e34b55..29c9a43a0c1 100644 --- a/app/views/projects/boards/index.html.haml +++ b/app/views/projects/boards/index.html.haml @@ -10,7 +10,9 @@ = render 'shared/issuable/filter', type: :boards -.boards-list#board-app{ "v-cloak" => true, data: board_data } - .boards-app-loading.text-center{ "v-if" => "loading" } - = icon("spinner spin") - = render "projects/boards/components/board" +#board-app.boards-app{ "v-cloak" => true, data: board_data } + .boards-list{ ":class" => "{ 'is-compact': detailIssueVisible }" } + .boards-app-loading.text-center{ "v-if" => "loading" } + = icon("spinner spin") + = render "projects/boards/components/board" + = render "projects/boards/components/sidebar" diff --git a/app/views/projects/boards/show.html.haml b/app/views/projects/boards/show.html.haml index 885f8e34b55..29c9a43a0c1 100644 --- a/app/views/projects/boards/show.html.haml +++ b/app/views/projects/boards/show.html.haml @@ -10,7 +10,9 @@ = render 'shared/issuable/filter', type: :boards -.boards-list#board-app{ "v-cloak" => true, data: board_data } - .boards-app-loading.text-center{ "v-if" => "loading" } - = icon("spinner spin") - = render "projects/boards/components/board" +#board-app.boards-app{ "v-cloak" => true, data: board_data } + .boards-list{ ":class" => "{ 'is-compact': detailIssueVisible }" } + .boards-app-loading.text-center{ "v-if" => "loading" } + = icon("spinner spin") + = render "projects/boards/components/board" + = render "projects/boards/components/sidebar" diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 55b6580e640..30473d14b9b 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -101,7 +101,7 @@ Git Large File Storage = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') - - if Gitlab.config.lfs.enabled && current_user.admin? + - if Gitlab.config.registry.enabled .form-group .checkbox = f.label :container_registry_enabled do diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 6f3f238a436..bd629b5c519 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -53,7 +53,7 @@ .issue-details.issuable-details - .detail-page-description.content-block + .detail-page-description.content-block{ class: ('hide-bottom-border' unless @issue.description.present? ) } %h2.title = markdown_field(@issue, :title) - if @issue.description.present? diff --git a/app/views/projects/merge_requests/show/_commits.html.haml b/app/views/projects/merge_requests/show/_commits.html.haml index 0b05785430b..61020516bcf 100644 --- a/app/views/projects/merge_requests/show/_commits.html.haml +++ b/app/views/projects/merge_requests/show/_commits.html.haml @@ -3,4 +3,4 @@ Most recent commits displayed first %ol#commits-list.list-unstyled - = render "projects/commits/commits", project: @merge_request.project + = render "projects/commits/commits", project: @merge_request.source_project diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index c83818e9199..f9ba77e87b5 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -31,7 +31,7 @@ = link_to namespace_project_milestone_path(@project.namespace, @project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-danger" do Delete - .detail-page-description.milestone-detail + .detail-page-description.milestone-detail{ class: ('hide-bottom-border' unless @milestone.description.present? ) } %h2.title = markdown_field(@milestone, :title) %div diff --git a/app/views/shared/icons/_icon_close.svg b/app/views/shared/icons/_icon_close.svg new file mode 100644 index 00000000000..9d62012518b --- /dev/null +++ b/app/views/shared/icons/_icon_close.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15"><path d="M9,7.5l5.83-5.91a.48.48,0,0,0,0-.69L14.11.15a.46.46,0,0,0-.68,0l-5.93,6L1.57.15a.46.46,0,0,0-.68,0L.15.9a.48.48,0,0,0,0,.69L6,7.5.15,13.41a.48.48,0,0,0,0,.69l.74.75a.46.46,0,0,0,.68,0l5.93-6,5.93,6a.46.46,0,0,0,.68,0l.74-.75a.48.48,0,0,0,0-.69Z"/></svg>
\ No newline at end of file diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb index 71b274e0c99..4dfa745fb50 100644 --- a/app/workers/project_cache_worker.rb +++ b/app/workers/project_cache_worker.rb @@ -9,6 +9,18 @@ class ProjectCacheWorker LEASE_TIMEOUT = 15.minutes.to_i + def self.lease_for(project_id) + Gitlab::ExclusiveLease. + new("project_cache_worker:#{project_id}", timeout: LEASE_TIMEOUT) + end + + # Overwrite Sidekiq's implementation so we only schedule when actually needed. + def self.perform_async(project_id) + # If a lease for this project is still being held there's no point in + # scheduling a new job. + super unless lease_for(project_id).exists? + end + def perform(project_id) if try_obtain_lease_for(project_id) Rails.logger. @@ -37,8 +49,6 @@ class ProjectCacheWorker end def try_obtain_lease_for(project_id) - Gitlab::ExclusiveLease. - new("project_cache_worker:#{project_id}", timeout: LEASE_TIMEOUT). - try_obtain + self.class.lease_for(project_id).try_obtain end end diff --git a/config/mail_room.yml b/config/mail_room.yml index c639f8260aa..68697bd1dc4 100644 --- a/config/mail_room.yml +++ b/config/mail_room.yml @@ -25,7 +25,7 @@ :delivery_options: :redis_url: <%= config[:redis_url].to_json %> :namespace: <%= Gitlab::Redis::SIDEKIQ_NAMESPACE %> - :queue: incoming_email + :queue: email_receiver :worker: EmailReceiverWorker :arbitration_method: redis diff --git a/config/routes/group.rb b/config/routes/group.rb index 826048ba196..4838c9d91c6 100644 --- a/config/routes/group.rb +++ b/config/routes/group.rb @@ -12,26 +12,23 @@ constraints(GroupUrlConstrainer.new) do end end -scope constraints: { id: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ } do - resources :groups, except: [:show] do - member do - get :issues - get :merge_requests - get :projects - get :activity - end +resources :groups, constraints: { id: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ } do + member do + get :issues + get :merge_requests + get :projects + get :activity + end - scope module: :groups do - resources :group_members, only: [:index, :create, :update, :destroy], concerns: :access_requestable do - post :resend_invite, on: :member - delete :leave, on: :collection - end + scope module: :groups do + resources :group_members, only: [:index, :create, :update, :destroy], concerns: :access_requestable do + post :resend_invite, on: :member + delete :leave, on: :collection + end - resource :avatar, only: [:destroy] - resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :update, :new, :create] + resource :avatar, only: [:destroy] + resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :update, :new, :create] - resources :labels, except: [:show], constraints: { id: /\d+/ } - end + resources :labels, except: [:show], constraints: { id: /\d+/ } end - get 'groups/:id' => 'groups#show', as: :group_canonical end diff --git a/db/migrate/20161024042317_migrate_mailroom_queue_from_default.rb b/db/migrate/20161024042317_migrate_mailroom_queue_from_default.rb new file mode 100644 index 00000000000..06d07bdb835 --- /dev/null +++ b/db/migrate/20161024042317_migrate_mailroom_queue_from_default.rb @@ -0,0 +1,63 @@ +require 'json' + +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class MigrateMailroomQueueFromDefault < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = true + + DOWNTIME_REASON = <<-EOF + Moving Sidekiq jobs from queues requires Sidekiq to be stopped. Not stopping + Sidekiq will result in the loss of jobs that are scheduled after this + migration completes. + EOF + + disable_ddl_transaction! + + # Jobs for which the queue names have been changed (e.g. multiple workers + # using the same non-default queue). + # + # The keys are the old queue names, the values the jobs to move and their new + # queue names. + RENAMED_QUEUES = { + incoming_email: { + 'EmailReceiverWorker' => :email_receiver + } + } + + def up + Sidekiq.redis do |redis| + RENAMED_QUEUES.each do |queue, jobs| + migrate_from_queue(redis, queue, jobs) + end + end + end + + def down + Sidekiq.redis do |redis| + RENAMED_QUEUES.each do |dest_queue, jobs| + jobs.each do |worker, from_queue| + migrate_from_queue(redis, from_queue, worker => dest_queue) + end + end + end + end + + def migrate_from_queue(redis, queue, job_mapping) + while job = redis.lpop("queue:#{queue}") + payload = JSON.load(job) + new_queue = job_mapping[payload['class']] + + # If we have no target queue to migrate to we're probably dealing with + # some ancient job for which the worker no longer exists. In that case + # there's no sane option we can take, other than just dropping the job. + next unless new_queue + + payload['queue'] = new_queue + + redis.lpush("queue:#{new_queue}", JSON.dump(payload)) + end + end +end diff --git a/db/schema.rb b/db/schema.rb index f5c01511195..02282b0f666 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20161019213545) do +ActiveRecord::Schema.define(version: 20161024042317) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" diff --git a/doc/administration/integration/koding.md b/doc/administration/integration/koding.md index a2c358af095..b95c425842c 100644 --- a/doc/administration/integration/koding.md +++ b/doc/administration/integration/koding.md @@ -61,6 +61,7 @@ executing commands in the following snippet. ```bash git clone https://github.com/koding/koding.git cd koding +docker-compose -f docker-compose-init.yml run init docker-compose up ``` diff --git a/doc/api/services.md b/doc/api/services.md index 579fdc0c8c9..c7f537aceb6 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -451,43 +451,49 @@ GET /projects/:id/services/irker ## JIRA -Jira issue tracker +JIRA issue tracker. + +### Get JIRA service settings + +Get JIRA service settings for a project. + +``` +GET /projects/:id/services/jira +``` ### Create/Edit JIRA service Set JIRA service for a project. -> Setting `project_url`, `issues_url` and `new_issue_url` will allow a user to easily navigate to the Jira issue tracker. See the [integration doc](http://docs.gitlab.com/ce/integration/external-issue-tracker.html) for details. Support for referencing commits and automatic closing of Jira issues directly from GitLab is [available in GitLab EE.](http://docs.gitlab.com/ee/integration/jira.html) +>**Note:** +Setting `project_url`, `issues_url` and `new_issue_url` will allow a user to +easily navigate to the JIRA issue tracker. See the [integration doc][jira-doc] +for details. ``` PUT /projects/:id/services/jira ``` -Parameters: - -- `new_issue_url` (**required**) - New Issue url -- `project_url` (**required**) - Project url -- `issues_url` (**required**) - Issue url -- `description` (optional) - Jira issue tracker -- `username` (optional) - Jira username -- `password` (optional) - Jira password +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `active` | boolean| no | Enable/disable the JIRA service. | +| `project_url` | string | yes | The URL to the JIRA project which is being linked to this GitLab project. It is of the form: `https://<jira_host_url>/issues/?jql=project=<jira_project>`. | +| `issues_url` | string | yes | The URL to the JIRA project issues overview for the project that is linked to this GitLab project. It is of the form: `https://<jira_host_url>/browse/:id`. Leave `:id` as-is, it gets replaced by GitLab at runtime.| +| `new_issue_url` | string | yes | This is the URL to create a new issue in JIRA for the project linked to this GitLab project, and it is of the form: `https://<jira_host_url>/secure/CreateIssue.jspa` | +| `api_url` | string | yes | The base URL of the JIRA API. It may be omitted, in which case GitLab will automatically use API version `2` based on the `project url`. It is of the form: `https://<jira_host_url>/rest/api/2`. | +| `description` | string | no | A name for the issue tracker. | +| `username` | string | no | The username of the user created to be used with GitLab/JIRA. | +| `password` | string | no | The password of the user created to be used with GitLab/JIRA. | +| `jira_issue_transition_id` | string | no | The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`. | ### Delete JIRA service -Delete JIRA service for a project. +Remove all previously JIRA settings from a project. ``` DELETE /projects/:id/services/jira ``` -### Get JIRA service settings - -Get JIRA service settings for a project. - -``` -GET /projects/:id/services/jira -``` - ## PivotalTracker Project Management Software (Source Commits Endpoint) @@ -662,3 +668,5 @@ Get JetBrains TeamCity CI service settings for a project. ``` GET /projects/:id/services/teamcity ``` + +[jira-doc]: ../project_services/jira.md diff --git a/doc/development/README.md b/doc/development/README.md index fb6a8a5b095..14d6f08e43a 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -8,6 +8,8 @@ ## Styleguides +- [API styleguide](api_styleguide.md) Use this styleguide if you are + contributing to the API. - [Documentation styleguide](doc_styleguide.md) Use this styleguide if you are contributing to documentation. - [SQL Migration Style Guide](migration_style_guide.md) for creating safe SQL migrations diff --git a/doc/development/api_styleguide.md b/doc/development/api_styleguide.md new file mode 100644 index 00000000000..ce444ebdde4 --- /dev/null +++ b/doc/development/api_styleguide.md @@ -0,0 +1,96 @@ +# API styleguide + +This styleguide recommends best practices for API development. + +## Instance variables + +Please do not use instance variables, there is no need for them (we don't need +to access them as we do in Rails views), local variables are fine. + +## Entities + +Always use an [Entity] to present the endpoint's payload. + +## Methods and parameters description + +Every method must be described using the [Grape DSL](https://github.com/ruby-grape/grape#describing-methods) +(see https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/api/environments.rb +for a good example): + +- `desc` for the method summary. You should pass it a block for additional + details such as: + - The GitLab version when the endpoint was added + - If the endpoint is deprecated, and if so, when will it be removed + +- `params` for the method params. This acts as description, + [validation, and coercion of the parameters] + +A good example is as follows: + +```ruby +desc 'Get all broadcast messages' do + detail 'This feature was introduced in GitLab 8.12.' + success Entities::BroadcastMessage +end +params do + optional :page, type: Integer, desc: 'Current page number' + optional :per_page, type: Integer, desc: 'Number of messages per page' +end +get do + messages = BroadcastMessage.all + + present paginate(messages), with: Entities::BroadcastMessage +end +``` + +## Declared params + +> Grape allows you to access only the parameters that have been declared by your +`params` block. It filters out the params that have been passed, but are not +allowed. + +– https://github.com/ruby-grape/grape#declared + +### Exclude params from parent namespaces! + +> By default `declared(params) `includes parameters that were defined in all +parent namespaces. + +– https://github.com/ruby-grape/grape#include-parent-namespaces + +In most cases you will want to exclude params from the parent namespaces: + +```ruby +declared(params, include_parent_namespaces: false) +``` + +### When to use `declared(params)`? + +You should always use `declared(params)` when you pass the params hash as +arguments to a method call. + +For instance: + +```ruby +# bad +User.create(params) # imagine the user submitted `admin=1`... :) + +# good +User.create(declared(params, include_parent_namespaces: false).to_h) +``` + +>**Note:** +`declared(params)` return a `Hashie::Mash` object, on which you will have to +call `.to_h`. + +But we can use `params[key]` directly when we access single elements. + +For instance: + +```ruby +# good +Model.create(foo: params[:foo]) +``` + +[Entity]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/api/entities.rb +[validation, and coercion of the parameters]: https://github.com/ruby-grape/grape#parameter-validation-and-coercion diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md index f07d2c9af2d..4cc581dd991 100644 --- a/doc/development/doc_styleguide.md +++ b/doc/development/doc_styleguide.md @@ -342,12 +342,6 @@ You can use the following fake tokens as examples. Here is a list of must-have items. Use them in the exact order that appears on this document. Further explanation is given below. -- Every method must be described using [Grape's DSL](https://github.com/ruby-grape/grape/tree/v0.13.0#describing-methods) - (see https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/api/environments.rb - for a good example): - - `desc` for the method summary (you can pass it a block for additional details) - - `params` for the method params (this acts as description **and** validation - of the params) - Every method must have the REST API request. For example: ``` diff --git a/doc/development/performance.md b/doc/development/performance.md index c4a964d1da3..8337c2d9cb3 100644 --- a/doc/development/performance.md +++ b/doc/development/performance.md @@ -37,7 +37,7 @@ graphs/dashboards. GitLab provides built-in tools to aid the process of improving performance: * [Sherlock](profiling.md#sherlock) -* [GitLab Performance Monitoring](../monitoring/performance/monitoring.md) +* [GitLab Performance Monitoring](../administration/monitoring/performance/monitoring.md) * [Request Profiling](../administration/monitoring/performance/request_profiling.md) GitLab employees can use GitLab.com's performance monitoring systems located at diff --git a/doc/install/installation.md b/doc/install/installation.md index c9acc9cdfb0..795e1d23443 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -142,6 +142,9 @@ gitlab-workhorse we need a Go compiler. The instructions below assume you use 64-bit Linux. You can find downloads for other platforms at the [Go download page](https://golang.org/dl). + # Remove former Go installation folder + sudo rm -rf /usr/local/go + curl --remote-name --progress https://storage.googleapis.com/golang/go1.5.3.linux-amd64.tar.gz echo '43afe0c5017e502630b1aea4d44b8a7f059bf60d7f29dfd58db454d4e4e0ae53 go1.5.3.linux-amd64.tar.gz' | shasum -a256 -c - && \ sudo tar -C /usr/local -xzf go1.5.3.linux-amd64.tar.gz diff --git a/doc/monitoring/performance/gitlab_configuration.md b/doc/monitoring/performance/gitlab_configuration.md index a669bb28904..19d46135930 100644 --- a/doc/monitoring/performance/gitlab_configuration.md +++ b/doc/monitoring/performance/gitlab_configuration.md @@ -1 +1 @@ -This document was moved to [administration/monitoring/performance/gitlab_configuration](../administration/monitoring/performance/gitlab_configuration.md). +This document was moved to [administration/monitoring/performance/gitlab_configuration](../../administration/monitoring/performance/gitlab_configuration.md). diff --git a/doc/monitoring/performance/influxdb_configuration.md b/doc/monitoring/performance/influxdb_configuration.md index 02647de1eb0..15fd275e916 100644 --- a/doc/monitoring/performance/influxdb_configuration.md +++ b/doc/monitoring/performance/influxdb_configuration.md @@ -1 +1 @@ -This document was moved to [administration/monitoring/performance/influxdb_configuration](../administration/monitoring/performance/influxdb_configuration.md). +This document was moved to [administration/monitoring/performance/influxdb_configuration](../../administration/monitoring/performance/influxdb_configuration.md). diff --git a/doc/monitoring/performance/influxdb_schema.md b/doc/monitoring/performance/influxdb_schema.md index a989e323e04..e53f9701dc3 100644 --- a/doc/monitoring/performance/influxdb_schema.md +++ b/doc/monitoring/performance/influxdb_schema.md @@ -1 +1 @@ -This document was moved to [administration/monitoring/performance/influxdb_schema](../administration/monitoring/performance/influxdb_schema.md). +This document was moved to [administration/monitoring/performance/influxdb_schema](../../administration/monitoring/performance/influxdb_schema.md). diff --git a/doc/monitoring/performance/introduction.md b/doc/monitoring/performance/introduction.md index ab3f3ac1664..ae88baa0c14 100644 --- a/doc/monitoring/performance/introduction.md +++ b/doc/monitoring/performance/introduction.md @@ -1 +1 @@ -This document was moved to [administration/monitoring/performance/introduction](../administration/monitoring/performance/introduction.md). +This document was moved to [administration/monitoring/performance/introduction](../../administration/monitoring/performance/introduction.md). diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 26baffdf792..0ad84705cfd 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -30,6 +30,10 @@ Use this if you've installed GitLab from source: ``` sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production ``` +If you are running GitLab within a Docker container, you can run the backup from the host: +``` +docker -t exec <container name> gitlab-rake gitlab:backup:create +``` You can specify that portions of the application data be skipped using the environment variable `SKIP`. You can skip: @@ -81,8 +85,11 @@ Deleting old backups... [SKIPPING] Starting with GitLab 7.4 you can let the backup script upload the '.tar' file it creates. It uses the [Fog library](http://fog.io/) to perform the upload. -In the example below we use Amazon S3 for storage. -Fog also supports [other storage providers](http://fog.io/storage/). +In the example below we use Amazon S3 for storage, but Fog also lets you use +[other storage providers](http://fog.io/storage/). GitLab +[imports cloud drivers](https://gitlab.com/gitlab-org/gitlab-ce/blob/30f5b9a5b711b46f1065baf755e413ceced5646b/Gemfile#L88) +for AWS, Azure, Google, OpenStack Swift and Rackspace as well. A local driver is +[also available](#uploading-to-locally-mounted-shares). For omnibus packages: diff --git a/doc/update/8.12-to-8.13.md b/doc/update/8.12-to-8.13.md index 8940d14559b..c0084d9d59c 100644 --- a/doc/update/8.12-to-8.13.md +++ b/doc/update/8.12-to-8.13.md @@ -72,7 +72,7 @@ sudo -u git -H git checkout 8-13-stable-ee ```bash cd /home/git/gitlab-shell sudo -u git -H git fetch --all --tags -sudo -u git -H git checkout v3.6.3 +sudo -u git -H git checkout v3.6.6 ``` ### 6. Update gitlab-workhorse diff --git a/doc/user/markdown.md b/doc/user/markdown.md index 56e5b802a52..7a7a0b864bd 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -501,6 +501,10 @@ There are two ways to create links, inline-style and reference-style. [I'm a reference-style link][Arbitrary case-insensitive reference text] [I'm a relative reference to a repository file](LICENSE) + + [I am an absolute reference within the repository](/doc/user/markdown.md) + + [I link to the Milestones page](/../milestones) [You can use numbers for reference-style link definitions][1] @@ -518,6 +522,10 @@ There are two ways to create links, inline-style and reference-style. [I'm a relative reference to a repository file](LICENSE)[^1] +[I am an absolute reference within the repository](/doc/user/markdown.md) + +[I link to the Milestones page](/../milestones) + [You can use numbers for reference-style link definitions][1] Or leave it empty and use the [link text itself][] diff --git a/doc/user/project/new_ci_build_permissions_model.md b/doc/user/project/new_ci_build_permissions_model.md index 8827b501901..60b7bec2ba7 100644 --- a/doc/user/project/new_ci_build_permissions_model.md +++ b/doc/user/project/new_ci_build_permissions_model.md @@ -254,6 +254,12 @@ test: This will make GitLab CI initialize (fetch) and update (checkout) all your submodules recursively. +If Git does not use the newly added relative URLs but still uses your old URLs, +you might need to add `git submodule sync --recursive` to your `.gitlab-ci.yml`, +prior to running `git submodule update --init --recursive`. This transfers the +changes from your `.gitmodules` file into the `.git` folder, which is kept by +runners between runs. + In case your environment or your Docker image doesn't have Git installed, you have to either ask your Administrator or install the missing dependency yourself: diff --git a/lib/api/branches.rb b/lib/api/branches.rb index b615703df93..6d827448994 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -54,43 +54,25 @@ module API not_found!('Branch') unless @branch protected_branch = user_project.protected_branches.find_by(name: @branch.name) - developers_can_merge = to_boolean(params[:developers_can_merge]) - developers_can_push = to_boolean(params[:developers_can_push]) - protected_branch_params = { - name: @branch.name + name: @branch.name, + developers_can_push: to_boolean(params[:developers_can_push]), + developers_can_merge: to_boolean(params[:developers_can_merge]) } - # If `developers_can_merge` is switched off, _all_ `DEVELOPER` - # merge_access_levels need to be deleted. - if developers_can_merge == false - protected_branch.merge_access_levels.where(access_level: Gitlab::Access::DEVELOPER).destroy_all - end + service_args = [user_project, current_user, protected_branch_params] - # If `developers_can_push` is switched off, _all_ `DEVELOPER` - # push_access_levels need to be deleted. - if developers_can_push == false - protected_branch.push_access_levels.where(access_level: Gitlab::Access::DEVELOPER).destroy_all - end + protected_branch = if protected_branch + ProtectedBranches::ApiUpdateService.new(*service_args).execute(protected_branch) + else + ProtectedBranches::ApiCreateService.new(*service_args).execute + end - protected_branch_params.merge!( - merge_access_levels_attributes: [{ - access_level: developers_can_merge ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER - }], - push_access_levels_attributes: [{ - access_level: developers_can_push ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER - }] - ) - - if protected_branch - service = ProtectedBranches::UpdateService.new(user_project, current_user, protected_branch_params) - service.execute(protected_branch) + if protected_branch.valid? + present @branch, with: Entities::RepoBranch, project: user_project else - service = ProtectedBranches::CreateService.new(user_project, current_user, protected_branch_params) - service.execute + render_api_error!(protected_branch.errors.full_messages, 422) end - - present @branch, with: Entities::RepoBranch, project: user_project end # Unprotect a single branch @@ -123,7 +105,7 @@ module API post ":id/repository/branches" do authorize_push_project result = CreateBranchService.new(user_project, current_user). - execute(params[:branch_name], params[:ref]) + execute(params[:branch_name], params[:ref]) if result[:status] == :success present result[:branch], @@ -142,10 +124,10 @@ module API # Example Request: # DELETE /projects/:id/repository/branches/:branch delete ":id/repository/branches/:branch", - requirements: { branch: /.+/ } do + requirements: { branch: /.+/ } do authorize_push_project result = DeleteBranchService.new(user_project, current_user). - execute(params[:branch]) + execute(params[:branch]) if result[:status] == :success { diff --git a/lib/api/builds.rb b/lib/api/builds.rb index 7b00c5037f1..67adca6605f 100644 --- a/lib/api/builds.rb +++ b/lib/api/builds.rb @@ -3,15 +3,32 @@ module API class Builds < Grape::API before { authenticate! } + params do + requires :id, type: String, desc: 'The ID of a project' + end resource :projects do - # Get a project builds - # - # Parameters: - # id (required) - The ID of a project - # scope (optional) - The scope of builds to show (one or array of: created, pending, running, failed, success, canceled, skipped; - # if none provided showing all builds) - # Example Request: - # GET /projects/:id/builds + helpers do + params :optional_scope do + optional :scope, types: [String, Array[String]], desc: 'The scope of builds to show', + values: ['pending', 'running', 'failed', 'success', 'canceled'], + coerce_with: ->(scope) { + if scope.is_a?(String) + [scope] + elsif scope.is_a?(Hashie::Mash) + scope.values + else + ['unknown'] + end + } + end + end + + desc 'Get a project builds' do + success Entities::Build + end + params do + use :optional_scope + end get ':id/builds' do builds = user_project.builds.order('id DESC') builds = filter_builds(builds, params[:scope]) @@ -20,15 +37,13 @@ module API user_can_download_artifacts: can?(current_user, :read_build, user_project) end - # Get builds for a specific commit of a project - # - # Parameters: - # id (required) - The ID of a project - # sha (required) - The SHA id of a commit - # scope (optional) - The scope of builds to show (one or array of: created, pending, running, failed, success, canceled, skipped; - # if none provided showing all builds) - # Example Request: - # GET /projects/:id/repository/commits/:sha/builds + desc 'Get builds for a specific commit of a project' do + success Entities::Build + end + params do + requires :sha, type: String, desc: 'The SHA id of a commit' + use :optional_scope + end get ':id/repository/commits/:sha/builds' do authorize_read_builds! @@ -42,13 +57,12 @@ module API user_can_download_artifacts: can?(current_user, :read_build, user_project) end - # Get a specific build of a project - # - # Parameters: - # id (required) - The ID of a project - # build_id (required) - The ID of a build - # Example Request: - # GET /projects/:id/builds/:build_id + desc 'Get a specific build of a project' do + success Entities::Build + end + params do + requires :build_id, type: Integer, desc: 'The ID of a build' + end get ':id/builds/:build_id' do authorize_read_builds! @@ -58,13 +72,12 @@ module API user_can_download_artifacts: can?(current_user, :read_build, user_project) end - # Download the artifacts file from build - # - # Parameters: - # id (required) - The ID of a build - # token (required) - The build authorization token - # Example Request: - # GET /projects/:id/builds/:build_id/artifacts + desc 'Download the artifacts file from build' do + detail 'This feature was introduced in GitLab 8.5' + end + params do + requires :build_id, type: Integer, desc: 'The ID of a build' + end get ':id/builds/:build_id/artifacts' do authorize_read_builds! @@ -73,14 +86,13 @@ module API present_artifacts!(build.artifacts_file) end - # Download the artifacts file from ref_name and job - # - # Parameters: - # id (required) - The ID of a project - # ref_name (required) - The ref from repository - # job (required) - The name for the build - # Example Request: - # GET /projects/:id/builds/artifacts/:ref_name/download?job=name + desc 'Download the artifacts file from build' do + detail 'This feature was introduced in GitLab 8.10' + end + params do + requires :ref_name, type: String, desc: 'The ref from repository' + requires :job, type: String, desc: 'The name for the build' + end get ':id/builds/artifacts/:ref_name/download', requirements: { ref_name: /.+/ } do authorize_read_builds! @@ -91,17 +103,13 @@ module API present_artifacts!(latest_build.artifacts_file) end - # Get a trace of a specific build of a project - # - # Parameters: - # id (required) - The ID of a project - # build_id (required) - The ID of a build - # Example Request: - # GET /projects/:id/build/:build_id/trace - # # TODO: We should use `present_file!` and leave this implementation for backward compatibility (when build trace # is saved in the DB instead of file). But before that, we need to consider how to replace the value of # `runners_token` with some mask (like `xxxxxx`) when sending trace file directly by workhorse. + desc 'Get a trace of a specific build of a project' + params do + requires :build_id, type: Integer, desc: 'The ID of a build' + end get ':id/builds/:build_id/trace' do authorize_read_builds! @@ -115,13 +123,12 @@ module API body trace end - # Cancel a specific build of a project - # - # parameters: - # id (required) - the id of a project - # build_id (required) - the id of a build - # example request: - # post /projects/:id/build/:build_id/cancel + desc 'Cancel a specific build of a project' do + success Entities::Build + end + params do + requires :build_id, type: Integer, desc: 'The ID of a build' + end post ':id/builds/:build_id/cancel' do authorize_update_builds! @@ -133,13 +140,12 @@ module API user_can_download_artifacts: can?(current_user, :read_build, user_project) end - # Retry a specific build of a project - # - # parameters: - # id (required) - the id of a project - # build_id (required) - the id of a build - # example request: - # post /projects/:id/build/:build_id/retry + desc 'Retry a specific build of a project' do + success Entities::Build + end + params do + requires :build_id, type: Integer, desc: 'The ID of a build' + end post ':id/builds/:build_id/retry' do authorize_update_builds! @@ -152,13 +158,12 @@ module API user_can_download_artifacts: can?(current_user, :read_build, user_project) end - # Erase build (remove artifacts and build trace) - # - # Parameters: - # id (required) - the id of a project - # build_id (required) - the id of a build - # example Request: - # post /projects/:id/build/:build_id/erase + desc 'Erase build (remove artifacts and build trace)' do + success Entities::Build + end + params do + requires :build_id, type: Integer, desc: 'The ID of a build' + end post ':id/builds/:build_id/erase' do authorize_update_builds! @@ -170,13 +175,12 @@ module API user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project) end - # Keep the artifacts to prevent them from being deleted - # - # Parameters: - # id (required) - the id of a project - # build_id (required) - The ID of a build - # Example Request: - # POST /projects/:id/builds/:build_id/artifacts/keep + desc 'Keep the artifacts to prevent them from being deleted' do + success Entities::Build + end + params do + requires :build_id, type: Integer, desc: 'The ID of a build' + end post ':id/builds/:build_id/artifacts/keep' do authorize_update_builds! @@ -235,14 +239,6 @@ module API return builds if scope.nil? || scope.empty? available_statuses = ::CommitStatus::AVAILABLE_STATUSES - scope = - if scope.is_a?(String) - [scope] - elsif scope.is_a?(Hashie::Mash) - scope.values - else - ['unknown'] - end unknown = scope - available_statuses render_api_error!('Scope contains invalid value(s)', 400) unless unknown.empty? diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 617a240318a..2f2cf769481 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -19,6 +19,7 @@ module API optional :until, type: String, desc: 'Only commits before or in this date will be returned' optional :page, type: Integer, default: 0, desc: 'The page for pagination' optional :per_page, type: Integer, default: 20, desc: 'The number of results per page' + optional :path, type: String, desc: 'The file path' end get ":id/repository/commits" do # TODO remove the next line for 9.0, use DateTime type in the params block @@ -28,6 +29,7 @@ module API offset = params[:page] * params[:per_page] commits = user_project.repository.commits(ref, + path: params[:path], limit: params[:per_page], offset: offset, after: params[:since], diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb index 825e05fbae3..425df2c176a 100644 --- a/lib/api/deploy_keys.rb +++ b/lib/api/deploy_keys.rb @@ -49,18 +49,23 @@ module API attrs = attributes_for_keys [:title, :key] attrs[:key].strip! if attrs[:key] + # Check for an existing key joined to this project key = user_project.deploy_keys.find_by(key: attrs[:key]) - present key, with: Entities::SSHKey if key + if key + present key, with: Entities::SSHKey + break + end # Check for available deploy keys in other projects key = current_user.accessible_deploy_keys.find_by(key: attrs[:key]) if key user_project.deploy_keys << key present key, with: Entities::SSHKey + break end + # Create a new deploy key key = DeployKey.new attrs - if key.valid? && user_project.deploy_keys << key present key, with: Entities::SSHKey else diff --git a/lib/api/labels.rb b/lib/api/labels.rb index 642e6345b9e..326e1e7ae00 100644 --- a/lib/api/labels.rb +++ b/lib/api/labels.rb @@ -3,37 +3,32 @@ module API class Labels < Grape::API before { authenticate! } + params do + requires :id, type: String, desc: 'The ID of a project' + end resource :projects do - # Get all labels of the project - # - # Parameters: - # id (required) - The ID of a project - # Example Request: - # GET /projects/:id/labels + desc 'Get all labels of the project' do + success Entities::Label + end get ':id/labels' do present available_labels, with: Entities::Label, current_user: current_user end - # Creates a new label - # - # Parameters: - # id (required) - The ID of a project - # name (required) - The name of the label to be created - # color (required) - Color of the label given in 6-digit hex - # notation with leading '#' sign (e.g. #FFAABB) - # description (optional) - The description of label to be created - # Example Request: - # POST /projects/:id/labels + desc 'Create a new label' do + success Entities::Label + end + params do + requires :name, type: String, desc: 'The name of the label to be created' + requires :color, type: String, desc: "The color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB)" + optional :description, type: String, desc: 'The description of label to be created' + end post ':id/labels' do authorize! :admin_label, user_project - required_attributes! [:name, :color] - - attrs = attributes_for_keys [:name, :color, :description] - label = user_project.find_label(attrs[:name]) + label = user_project.find_label(params[:name]) conflict!('Label already exists') if label - label = user_project.labels.create(attrs) + label = user_project.labels.create(declared(params, include_parent_namespaces: false).to_h) if label.valid? present label, with: Entities::Label, current_user: current_user @@ -42,54 +37,44 @@ module API end end - # Deletes an existing label - # - # Parameters: - # id (required) - The ID of a project - # name (required) - The name of the label to be deleted - # - # Example Request: - # DELETE /projects/:id/labels + desc 'Delete an existing label' do + success Entities::Label + end + params do + requires :name, type: String, desc: 'The name of the label to be deleted' + end delete ':id/labels' do authorize! :admin_label, user_project - required_attributes! [:name] label = user_project.find_label(params[:name]) not_found!('Label') unless label - label.destroy + present label.destroy, with: Entities::Label, current_user: current_user end - # Updates an existing label. At least one optional parameter is required. - # - # Parameters: - # id (required) - The ID of a project - # name (required) - The name of the label to be deleted - # new_name (optional) - The new name of the label - # color (optional) - Color of the label given in 6-digit hex - # notation with leading '#' sign (e.g. #FFAABB) - # description (optional) - The description of label to be created - # Example Request: - # PUT /projects/:id/labels + desc 'Update an existing label. At least one optional parameter is required.' do + success Entities::Label + end + params do + requires :name, type: String, desc: 'The name of the label to be updated' + optional :new_name, type: String, desc: 'The new name of the label' + optional :color, type: String, desc: "The new color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB)" + optional :description, type: String, desc: 'The new description of label' + at_least_one_of :new_name, :color, :description + end put ':id/labels' do authorize! :admin_label, user_project - required_attributes! [:name] label = user_project.find_label(params[:name]) not_found!('Label not found') unless label - attrs = attributes_for_keys [:new_name, :color, :description] - - if attrs.empty? - render_api_error!('Required parameters "new_name" or "color" ' \ - 'missing', - 400) - end - + update_params = declared(params, + include_parent_namespaces: false, + include_missing: false).to_h # Rename new name to the actual label attribute name - attrs[:name] = attrs.delete(:new_name) if attrs.key?(:new_name) + update_params['name'] = update_params.delete('new_name') if update_params.key?('new_name') - if label.update(attrs) + if label.update(update_params) present label, with: Entities::Label, current_user: current_user else render_validation_error!(label) diff --git a/lib/api/tags.rb b/lib/api/tags.rb index 7b675e05fbb..bf2a199ce21 100644 --- a/lib/api/tags.rb +++ b/lib/api/tags.rb @@ -4,25 +4,24 @@ module API before { authenticate! } before { authorize! :download_code, user_project } + params do + requires :id, type: String, desc: 'The ID of a project' + end resource :projects do - # Get a project repository tags - # - # Parameters: - # id (required) - The ID of a project - # Example Request: - # GET /projects/:id/repository/tags + desc 'Get a project repository tags' do + success Entities::RepoTag + end get ":id/repository/tags" do present user_project.repository.tags.sort_by(&:name).reverse, with: Entities::RepoTag, project: user_project end - # Get a single repository tag - # - # Parameters: - # id (required) - The ID of a project - # tag_name (required) - The name of the tag - # Example Request: - # GET /projects/:id/repository/tags/:tag_name + desc 'Get a single repository tag' do + success Entities::RepoTag + end + params do + requires :tag_name, type: String, desc: 'The name of the tag' + end get ":id/repository/tags/:tag_name", requirements: { tag_name: /.+/ } do tag = user_project.repository.find_tag(params[:tag_name]) not_found!('Tag') unless tag @@ -30,20 +29,21 @@ module API present tag, with: Entities::RepoTag, project: user_project end - # Create tag - # - # Parameters: - # id (required) - The ID of a project - # tag_name (required) - The name of the tag - # ref (required) - Create tag from commit sha or branch - # message (optional) - Specifying a message creates an annotated tag. - # Example Request: - # POST /projects/:id/repository/tags + desc 'Create a new repository tag' do + success Entities::RepoTag + end + params do + requires :tag_name, type: String, desc: 'The name of the tag' + requires :ref, type: String, desc: 'The commit sha or branch name' + optional :message, type: String, desc: 'Specifying a message creates an annotated tag' + optional :release_description, type: String, desc: 'Specifying release notes stored in the GitLab database' + end post ':id/repository/tags' do authorize_push_project - message = params[:message] || nil + create_params = declared(params) + result = CreateTagService.new(user_project, current_user). - execute(params[:tag_name], params[:ref], message, params[:release_description]) + execute(create_params[:tag_name], create_params[:ref], create_params[:message], create_params[:release_description]) if result[:status] == :success present result[:tag], @@ -54,15 +54,13 @@ module API end end - # Delete tag - # - # Parameters: - # id (required) - The ID of a project - # tag_name (required) - The name of the tag - # Example Request: - # DELETE /projects/:id/repository/tags/:tag + desc 'Delete a repository tag' + params do + requires :tag_name, type: String, desc: 'The name of the tag' + end delete ":id/repository/tags/:tag_name", requirements: { tag_name: /.+/ } do authorize_push_project + result = DeleteTagService.new(user_project, current_user). execute(params[:tag_name]) @@ -75,17 +73,16 @@ module API end end - # Add release notes to tag - # - # Parameters: - # id (required) - The ID of a project - # tag_name (required) - The name of the tag - # description (required) - Release notes with markdown support - # Example Request: - # POST /projects/:id/repository/tags/:tag_name/release + desc 'Add a release note to a tag' do + success Entities::Release + end + params do + requires :tag_name, type: String, desc: 'The name of the tag' + requires :description, type: String, desc: 'Release notes with markdown support' + end post ':id/repository/tags/:tag_name/release', requirements: { tag_name: /.+/ } do authorize_push_project - required_attributes! [:description] + result = CreateReleaseService.new(user_project, current_user). execute(params[:tag_name], params[:description]) @@ -96,17 +93,16 @@ module API end end - # Updates a release notes of a tag - # - # Parameters: - # id (required) - The ID of a project - # tag_name (required) - The name of the tag - # description (required) - Release notes with markdown support - # Example Request: - # PUT /projects/:id/repository/tags/:tag_name/release + desc "Update a tag's release note" do + success Entities::Release + end + params do + requires :tag_name, type: String, desc: 'The name of the tag' + requires :description, type: String, desc: 'Release notes with markdown support' + end put ':id/repository/tags/:tag_name/release', requirements: { tag_name: /.+/ } do authorize_push_project - required_attributes! [:description] + result = UpdateReleaseService.new(user_project, current_user). execute(params[:tag_name], params[:description]) diff --git a/lib/api/users.rb b/lib/api/users.rb index e868f628404..c28e07a76b7 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -333,11 +333,11 @@ module API user = User.find_by(id: declared(params).id) not_found!('User') unless user - events = user.recent_events. + events = user.events. merge(ProjectsFinder.new.execute(current_user)). references(:project). with_associations. - page(params[:page]) + recent present paginate(events), with: Entities::Event end diff --git a/lib/banzai/filter/relative_link_filter.rb b/lib/banzai/filter/relative_link_filter.rb index 4fa8d05481f..f09d78be0ce 100644 --- a/lib/banzai/filter/relative_link_filter.rb +++ b/lib/banzai/filter/relative_link_filter.rb @@ -52,8 +52,8 @@ module Banzai relative_url_root, context[:project].path_with_namespace, uri_type(file_path), - ref, - file_path + Addressable::URI.escape(ref), + Addressable::URI.escape(file_path) ].compact.join('/').squeeze('/').chomp('/') uri diff --git a/lib/constraints/namespace_url_constrainer.rb b/lib/constraints/namespace_url_constrainer.rb index 23920193743..91b70143f11 100644 --- a/lib/constraints/namespace_url_constrainer.rb +++ b/lib/constraints/namespace_url_constrainer.rb @@ -1,6 +1,9 @@ class NamespaceUrlConstrainer def matches?(request) - id = request.path.sub(/\A\/+/, '').split('/').first.sub(/.atom\z/, '') + id = request.path + id = id.sub(/\A#{relative_url_root}/, '') if relative_url_root + id = id.sub(/\A\/+/, '').split('/').first + id = id.sub(/.atom\z/, '') if id if id =~ Gitlab::Regex.namespace_regex find_resource(id) @@ -10,4 +13,12 @@ class NamespaceUrlConstrainer def find_resource(id) Namespace.find_by_path(id) end + + private + + def relative_url_root + if defined?(Gitlab::Application.config.relative_url_root) + Gitlab::Application.config.relative_url_root + end + end end diff --git a/lib/gitlab/ee_compat_check.rb b/lib/gitlab/ee_compat_check.rb new file mode 100644 index 00000000000..b1a6d5fe0f6 --- /dev/null +++ b/lib/gitlab/ee_compat_check.rb @@ -0,0 +1,261 @@ +# rubocop: disable Rails/Output +module Gitlab + # Checks if a set of migrations requires downtime or not. + class EeCompatCheck + EE_REPO = 'https://gitlab.com/gitlab-org/gitlab-ee.git'.freeze + + attr_reader :ce_branch, :check_dir, :ce_repo + + def initialize(branch:, check_dir:, ce_repo: nil) + @ce_branch = branch + @check_dir = check_dir + @ce_repo = ce_repo || 'https://gitlab.com/gitlab-org/gitlab-ce.git' + end + + def check + ensure_ee_repo + delete_patches + + generate_patch(ce_branch, ce_patch_full_path) + + Dir.chdir(check_dir) do + step("In the #{check_dir} directory") + + step("Pulling latest master", %w[git pull --ff-only origin master]) + + status = catch(:halt_check) do + ce_branch_compat_check! + + delete_ee_branch_locally + + ee_branch_presence_check! + + ee_branch_compat_check! + end + + delete_ee_branch_locally + delete_patches + + if status.nil? + true + else + false + end + end + end + + private + + def ensure_ee_repo + if Dir.exist?(check_dir) + step("#{check_dir} already exists") + else + cmd = %W[git clone --branch master --single-branch --depth 1 #{EE_REPO} #{check_dir}] + step("Cloning #{EE_REPO} into #{check_dir}", cmd) + end + end + + def ce_branch_compat_check! + cmd = %W[git apply --check #{ce_patch_full_path}] + status = step("Checking if #{ce_patch_name} applies cleanly to EE/master", cmd) + + if status.zero? + puts ce_applies_cleanly_msg(ce_branch) + throw(:halt_check) + end + end + + def ee_branch_presence_check! + status = step("Fetching origin/#{ee_branch}", %W[git fetch origin #{ee_branch}]) + + unless status.zero? + puts + puts ce_branch_doesnt_apply_cleanly_and_no_ee_branch_msg + + throw(:halt_check, :ko) + end + end + + def ee_branch_compat_check! + step("Checking out origin/#{ee_branch}", %W[git checkout -b #{ee_branch} FETCH_HEAD]) + + generate_patch(ee_branch, ee_patch_full_path) + cmd = %W[git apply --check #{ee_patch_full_path}] + status = step("Checking if #{ee_patch_name} applies cleanly to EE/master", cmd) + + unless status.zero? + puts + puts ee_branch_doesnt_apply_cleanly_msg + + throw(:halt_check, :ko) + end + + puts + puts ee_applies_cleanly_msg + end + + def generate_patch(branch, filepath) + FileUtils.rm(filepath, force: true) + + depth = 0 + loop do + depth += 10 + step("Fetching origin/master", %W[git fetch origin master --depth=#{depth}]) + status = step("Finding merge base with master", %W[git merge-base FETCH_HEAD #{branch}]) + + break if status.zero? || depth > 500 + end + + raise "#{branch} is too far behind master, please rebase it!" if depth > 500 + + step("Generating the patch against master") + output, status = Gitlab::Popen.popen(%w[git format-patch FETCH_HEAD --stdout]) + throw(:halt_check, :ko) unless status.zero? + + File.write(filepath, output) + throw(:halt_check, :ko) unless File.exist?(filepath) + end + + def delete_ee_branch_locally + command(%w[git checkout master]) + step("Deleting the local #{ee_branch} branch", %W[git branch -D #{ee_branch}]) + end + + def delete_patches + step("Deleting #{ce_patch_full_path}") + FileUtils.rm(ce_patch_full_path, force: true) + + step("Deleting #{ee_patch_full_path}") + FileUtils.rm(ee_patch_full_path, force: true) + end + + def ce_patch_name + @ce_patch_name ||= "#{ce_branch}.patch" + end + + def ce_patch_full_path + @ce_patch_full_path ||= File.expand_path(ce_patch_name, check_dir) + end + + def ee_branch + @ee_branch ||= "#{ce_branch}-ee" + end + + def ee_patch_name + @ee_patch_name ||= "#{ee_branch}.patch" + end + + def ee_patch_full_path + @ee_patch_full_path ||= File.expand_path(ee_patch_name, check_dir) + end + + def step(desc, cmd = nil) + puts "\n=> #{desc}\n" + + if cmd + puts "\n$ #{cmd.join(' ')}" + command(cmd) + end + end + + def command(cmd) + output, status = Gitlab::Popen.popen(cmd) + puts output + + status + end + + def ce_applies_cleanly_msg(ce_branch) + <<-MSG.strip_heredoc + ================================================================= + 🎉 Congratulations!! 🎉 + + The #{ce_branch} branch applies cleanly to EE/master! + + Much ❤️!! + =================================================================\n + MSG + end + + def ce_branch_doesnt_apply_cleanly_and_no_ee_branch_msg + <<-MSG.strip_heredoc + ================================================================= + 💥 Oh no! 💥 + + The #{ce_branch} branch does not apply cleanly to the current + EE/master, and no #{ee_branch} branch was found in the EE repository. + + Please create a #{ee_branch} branch that includes changes from + #{ce_branch} but also specific changes than can be applied cleanly + to EE/master. + + There are different ways to create such branch: + + 1. Create a new branch based on the CE branch and rebase it on top of EE/master + + # In the EE repo + $ git fetch #{ce_repo} #{ce_branch} + $ git checkout -b #{ee_branch} FETCH_HEAD + + # You can squash the #{ce_branch} commits into a single "Port of #{ce_branch} to EE" commit + # before rebasing to limit the conflicts-resolving steps during the rebase + $ git fetch origin + $ git rebase origin/master + + At this point you will likely have conflicts. + Solve them, and continue/finish the rebase. + + You can squash the #{ce_branch} commits into a single "Port of #{ce_branch} to EE". + + 2. Create a new branch from master and cherry-pick your CE commits + + # In the EE repo + $ git fetch origin + $ git checkout -b #{ee_branch} FETCH_HEAD + $ git fetch #{ce_repo} #{ce_branch} + $ git cherry-pick SHA # Repeat for all the commits you want to pick + + You can squash the #{ce_branch} commits into a single "Port of #{ce_branch} to EE" commit. + + Don't forget to push your branch to #{EE_REPO}: + + # In the EE repo + $ git push origin #{ee_branch} + + You can then retry this failed build, and hopefully it should pass. + + Stay 💪 ! + =================================================================\n + MSG + end + + def ee_branch_doesnt_apply_cleanly_msg + <<-MSG.strip_heredoc + ================================================================= + 💥 Oh no! 💥 + + The #{ce_branch} does not apply cleanly to the current + EE/master, and even though a #{ee_branch} branch exists in the EE + repository, it does not apply cleanly either to EE/master! + + Please update the #{ee_branch}, push it again to #{EE_REPO}, and + retry this build. + + Stay 💪 ! + =================================================================\n + MSG + end + + def ee_applies_cleanly_msg + <<-MSG.strip_heredoc + ================================================================= + 🎉 Congratulations!! 🎉 + + The #{ee_branch} branch applies cleanly to EE/master! + + Much ❤️!! + =================================================================\n + MSG + end + end +end diff --git a/lib/gitlab/exclusive_lease.rb b/lib/gitlab/exclusive_lease.rb index ffe49364379..7e8f35e9298 100644 --- a/lib/gitlab/exclusive_lease.rb +++ b/lib/gitlab/exclusive_lease.rb @@ -27,7 +27,7 @@ module Gitlab # on begin/ensure blocks to cancel a lease, because the 'ensure' does # not always run. Think of 'kill -9' from the Unicorn master for # instance. - # + # # If you find that leases are getting in your way, ask yourself: would # it be enough to lower the lease timeout? Another thing that might be # appropriate is to only use a lease for bulk/automated operations, and @@ -48,6 +48,13 @@ module Gitlab end end + # Returns true if the key for this lease is set. + def exists? + Gitlab::Redis.with do |redis| + redis.exists(redis_key) + end + end + # No #cancel method. See comments above! private diff --git a/lib/tasks/ce_to_ee_merge_check.rake b/lib/tasks/ce_to_ee_merge_check.rake deleted file mode 100644 index 424e7883060..00000000000 --- a/lib/tasks/ce_to_ee_merge_check.rake +++ /dev/null @@ -1,4 +0,0 @@ -desc 'Checks if the branch would apply cleanly to EE' -task ce_to_ee_merge_check: :environment do - Rake::Task['gitlab:dev:ce_to_ee_merge_check'].invoke -end diff --git a/lib/tasks/ee_compat_check.rake b/lib/tasks/ee_compat_check.rake new file mode 100644 index 00000000000..f494fa5c5c2 --- /dev/null +++ b/lib/tasks/ee_compat_check.rake @@ -0,0 +1,4 @@ +desc 'Checks if the branch would apply cleanly to EE' +task ee_compat_check: :environment do + Rake::Task['gitlab:dev:ee_compat_check'].invoke +end diff --git a/lib/tasks/gitlab/dev.rake b/lib/tasks/gitlab/dev.rake index 47bdb2d32d2..5ee99dfc810 100644 --- a/lib/tasks/gitlab/dev.rake +++ b/lib/tasks/gitlab/dev.rake @@ -1,106 +1,21 @@ namespace :gitlab do namespace :dev do desc 'Checks if the branch would apply cleanly to EE' - task ce_to_ee_merge_check: :environment do + task ee_compat_check: :environment do return if defined?(Gitlab::License) return unless ENV['CI'] - ce_repo = ENV['CI_BUILD_REPO'] - ce_branch = ENV['CI_BUILD_REF_NAME'] - - ee_repo = 'https://gitlab.com/gitlab-org/gitlab-ee.git' - ee_branch = "#{ce_branch}-ee" - ee_dir = 'gitlab-ee-merge-check' - - puts "\n=> Cloning #{ee_repo} into #{ee_dir}\n" - `git clone #{ee_repo} #{ee_dir} --depth 1` - Dir.chdir(ee_dir) do - puts "\n => Fetching #{ce_repo}/#{ce_branch}\n" - `git fetch #{ce_repo} #{ce_branch} --depth 1` - - # Try to merge the current tested branch to EE/master... - puts "\n => Merging #{ce_repo}/#{ce_branch} into #{ee_repo}/master\n" - `git merge FETCH_HEAD` - - exit 0 if $?.success? - - # Check if the <branch>-ee branch exists... - puts "\n => Check if #{ee_repo}/#{ee_branch} exists\n" - `git rev-parse --verify #{ee_branch}` - - # The <branch>-ee doesn't exist - unless $?.success? - puts - puts <<-MSG.strip_heredoc - ================================================================= - The #{ce_branch} branch cannot be merged without conflicts to the - current EE/master, and no #{ee_branch} branch was detected in - the EE repository. - - Please create a #{ee_branch} branch that includes changes from - #{ce_branch} but also specific changes than can be applied cleanly - to EE/master. - - You can create this branch as follows: - - 1. In the EE repo: - $ git fetch origin - $ git fetch #{ce_repo} #{ce_branch} - $ git checkout -b #{ee_branch} FETCH_HEAD - $ git rebase origin/master - 2. At this point you will likely have conflicts, solve them, and - continue/finish the rebase. Note: You can squash the CE commits - before rebasing. - 3. You can squash all the original #{ce_branch} commits into a - single "Port of #{ce_branch} to EE". - 4. Push your branch to #{ee_repo}: - $ git push origin #{ee_branch} - =================================================================\n - MSG - - exit 1 - end - - # Try to merge the <branch>-ee branch to EE/master... - puts "\n => Merging #{ee_repo}/#{ee_branch} into #{ee_repo}/master\n" - `git merge #{ee_branch} master` - - # The <branch>-ee cannot be merged cleanly to EE/master... - unless $?.success? - puts - puts <<-MSG.strip_heredoc - ================================================================= - The #{ce_branch} branch cannot be merged without conflicts to - EE/master, and even though the #{ee_branch} branch exists in the EE - repository, it cannot be merged without conflicts to EE/master. - - Please update the #{ee_branch}, push it again to #{ee_repo}, and - retry this job. - =================================================================\n - MSG - - exit 2 - end - - puts "\n => Merging #{ce_repo}/#{ce_branch} into #{ee_repo}/master\n" - `git merge FETCH_HEAD` - exit 0 if $?.success? - - # The <branch>-ee can be merged cleanly to EE/master, but <branch> still - # cannot be merged cleanly to EE/master... - puts - puts <<-MSG.strip_heredoc - ================================================================= - The #{ce_branch} branch cannot be merged without conflicts to EE, and - even though the #{ee_branch} branch exists in the EE repository and - applies cleanly to EE/master, it doesn't prevent conflicts when - merging #{ce_branch} into EE. - - We may be in a complex situation here. - =================================================================\n - MSG - - exit 3 + success = + Gitlab::EeCompatCheck.new( + branch: ENV['CI_BUILD_REF_NAME'], + check_dir: File.expand_path('ee-compat-check', __dir__), + ce_repo: ENV['CI_BUILD_REPO'] + ).check + + if success + exit 0 + else + exit 1 end end end diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb index ad15b3f8f40..c7db84dd5f9 100644 --- a/spec/controllers/groups/group_members_controller_spec.rb +++ b/spec/controllers/groups/group_members_controller_spec.rb @@ -13,6 +13,49 @@ describe Groups::GroupMembersController do end end + describe 'POST create' do + let(:group_user) { create(:user) } + + before { sign_in(user) } + + context 'when user does not have enough rights' do + before { group.add_developer(user) } + + it 'returns 403' do + post :create, group_id: group, + user_ids: group_user.id, + access_level: Gitlab::Access::GUEST + + expect(response).to have_http_status(403) + expect(group.users).not_to include group_user + end + end + + context 'when user has enough rights' do + before { group.add_owner(user) } + + it 'adds user to members' do + post :create, group_id: group, + user_ids: group_user.id, + access_level: Gitlab::Access::GUEST + + expect(response).to set_flash.to 'Users were successfully added.' + expect(response).to redirect_to(group_group_members_path(group)) + expect(group.users).to include group_user + end + + it 'adds no user to members' do + post :create, group_id: group, + user_ids: '', + access_level: Gitlab::Access::GUEST + + expect(response).to set_flash.to 'No users specified.' + expect(response).to redirect_to(group_group_members_path(group)) + expect(group.users).not_to include group_user + end + end + end + describe 'DELETE destroy' do let(:member) { create(:group_member, :developer, group: group) } diff --git a/spec/controllers/projects/boards/issues_controller_spec.rb b/spec/controllers/projects/boards/issues_controller_spec.rb index da59642f24d..cbe0417a4a7 100644 --- a/spec/controllers/projects/boards/issues_controller_spec.rb +++ b/spec/controllers/projects/boards/issues_controller_spec.rb @@ -21,9 +21,11 @@ describe Projects::Boards::IssuesController do context 'with valid list id' do it 'returns issues that have the list label applied' do johndoe = create(:user, avatar: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png'))) + issue = create(:labeled_issue, project: project, labels: [planning]) create(:labeled_issue, project: project, labels: [planning]) - create(:labeled_issue, project: project, labels: [development]) + create(:labeled_issue, project: project, labels: [development], due_date: Date.tomorrow) create(:labeled_issue, project: project, labels: [development], assignee: johndoe) + issue.subscribe(johndoe) list_issues user: user, board: board, list: list2 diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb index 5e487241d07..b4f066d8600 100644 --- a/spec/controllers/projects/project_members_controller_spec.rb +++ b/spec/controllers/projects/project_members_controller_spec.rb @@ -13,6 +13,54 @@ describe Projects::ProjectMembersController do end end + describe 'POST create' do + context 'when users are added' do + let(:project_user) { create(:user) } + + before { sign_in(user) } + + context 'when user does not have enough rights' do + before { project.team << [user, :developer] } + + it 'returns 404' do + post :create, namespace_id: project.namespace, + project_id: project, + user_ids: project_user.id, + access_level: Gitlab::Access::GUEST + + expect(response).to have_http_status(404) + expect(project.users).not_to include project_user + end + end + + context 'when user has enough rights' do + before { project.team << [user, :master] } + + it 'adds user to members' do + post :create, namespace_id: project.namespace, + project_id: project, + user_ids: project_user.id, + access_level: Gitlab::Access::GUEST + + expect(response).to set_flash.to 'Users were successfully added.' + expect(response).to redirect_to(namespace_project_project_members_path(project.namespace, project)) + expect(project.users).to include project_user + end + + it 'adds no user to members' do + post :create, namespace_id: project.namespace, + project_id: project, + user_ids: '', + access_level: Gitlab::Access::GUEST + + expect(response).to set_flash.to 'No users or groups specified.' + expect(response).to redirect_to(namespace_project_project_members_path(project.namespace, project)) + expect(project.users).not_to include project_user + end + end + end + end + describe 'DELETE destroy' do let(:member) { create(:project_member, :developer, project: project) } diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb index 41d263a46a4..2d762fdaa04 100644 --- a/spec/controllers/snippets_controller_spec.rb +++ b/spec/controllers/snippets_controller_spec.rb @@ -116,116 +116,126 @@ describe SnippetsController do end end - describe 'GET #raw' do - let(:user) { create(:user) } + %w(raw download).each do |action| + describe "GET #{action}" do + context 'when the personal snippet is private' do + let(:personal_snippet) { create(:personal_snippet, :private, author: user) } + + context 'when signed in' do + before do + sign_in(user) + end - context 'when the personal snippet is private' do - let(:personal_snippet) { create(:personal_snippet, :private, author: user) } + context 'when signed in user is not the author' do + let(:other_author) { create(:author) } + let(:other_personal_snippet) { create(:personal_snippet, :private, author: other_author) } - context 'when signed in' do - before do - sign_in(user) - end + it 'responds with status 404' do + get action, id: other_personal_snippet.to_param - context 'when signed in user is not the author' do - let(:other_author) { create(:author) } - let(:other_personal_snippet) { create(:personal_snippet, :private, author: other_author) } + expect(response).to have_http_status(404) + end + end - it 'responds with status 404' do - get :raw, id: other_personal_snippet.to_param + context 'when signed in user is the author' do + before { get action, id: personal_snippet.to_param } - expect(response).to have_http_status(404) - end - end + it 'responds with status 200' do + expect(assigns(:snippet)).to eq(personal_snippet) + expect(response).to have_http_status(200) + end - context 'when signed in user is the author' do - it 'renders the raw snippet' do - get :raw, id: personal_snippet.to_param + it 'has expected headers' do + expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8') - expect(assigns(:snippet)).to eq(personal_snippet) - expect(response).to have_http_status(200) + if action == :download + expect(response.header['Content-Disposition']).to match(/attachment/) + elsif action == :raw + expect(response.header['Content-Disposition']).to match(/inline/) + end + end end end - end - context 'when not signed in' do - it 'redirects to the sign in page' do - get :raw, id: personal_snippet.to_param + context 'when not signed in' do + it 'redirects to the sign in page' do + get action, id: personal_snippet.to_param - expect(response).to redirect_to(new_user_session_path) + expect(response).to redirect_to(new_user_session_path) + end end end - end - context 'when the personal snippet is internal' do - let(:personal_snippet) { create(:personal_snippet, :internal, author: user) } + context 'when the personal snippet is internal' do + let(:personal_snippet) { create(:personal_snippet, :internal, author: user) } - context 'when signed in' do - before do - sign_in(user) - end + context 'when signed in' do + before do + sign_in(user) + end - it 'renders the raw snippet' do - get :raw, id: personal_snippet.to_param + it 'responds with status 200' do + get action, id: personal_snippet.to_param - expect(assigns(:snippet)).to eq(personal_snippet) - expect(response).to have_http_status(200) + expect(assigns(:snippet)).to eq(personal_snippet) + expect(response).to have_http_status(200) + end end - end - context 'when not signed in' do - it 'redirects to the sign in page' do - get :raw, id: personal_snippet.to_param + context 'when not signed in' do + it 'redirects to the sign in page' do + get action, id: personal_snippet.to_param - expect(response).to redirect_to(new_user_session_path) + expect(response).to redirect_to(new_user_session_path) + end end end - end - context 'when the personal snippet is public' do - let(:personal_snippet) { create(:personal_snippet, :public, author: user) } + context 'when the personal snippet is public' do + let(:personal_snippet) { create(:personal_snippet, :public, author: user) } - context 'when signed in' do - before do - sign_in(user) - end + context 'when signed in' do + before do + sign_in(user) + end - it 'renders the raw snippet' do - get :raw, id: personal_snippet.to_param + it 'responds with status 200' do + get action, id: personal_snippet.to_param - expect(assigns(:snippet)).to eq(personal_snippet) - expect(response).to have_http_status(200) + expect(assigns(:snippet)).to eq(personal_snippet) + expect(response).to have_http_status(200) + end end - end - context 'when not signed in' do - it 'renders the raw snippet' do - get :raw, id: personal_snippet.to_param + context 'when not signed in' do + it 'responds with status 200' do + get action, id: personal_snippet.to_param - expect(assigns(:snippet)).to eq(personal_snippet) - expect(response).to have_http_status(200) + expect(assigns(:snippet)).to eq(personal_snippet) + expect(response).to have_http_status(200) + end end end - end - context 'when the personal snippet does not exist' do - context 'when signed in' do - before do - sign_in(user) - end + context 'when the personal snippet does not exist' do + context 'when signed in' do + before do + sign_in(user) + end - it 'responds with status 404' do - get :raw, id: 'doesntexist' + it 'responds with status 404' do + get action, id: 'doesntexist' - expect(response).to have_http_status(404) + expect(response).to have_http_status(404) + end end - end - context 'when not signed in' do - it 'responds with status 404' do - get :raw, id: 'doesntexist' + context 'when not signed in' do + it 'responds with status 404' do + get action, id: 'doesntexist' - expect(response).to have_http_status(404) + expect(response).to have_http_status(404) + end end end end diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index 0fb1608a0a3..c533ce1d87f 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -624,6 +624,10 @@ describe 'Issue Boards', feature: true, js: true do it 'does not show create new list' do expect(page).not_to have_selector('.js-new-board-list') end + + it 'does not allow dragging' do + expect(page).not_to have_selector('.user-can-drag') + end end context 'as guest user' do diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb index 67d6da5f39a..760a8967123 100644 --- a/spec/features/boards/new_issue_spec.rb +++ b/spec/features/boards/new_issue_spec.rb @@ -66,6 +66,21 @@ describe 'Issue Boards new issue', feature: true, js: true do expect(page).to have_content('1') end end + + it 'shows sidebar when creating new issue' do + page.within(first('.board')) do + find('.board-issue-count-holder .btn').click + end + + page.within(first('.board-new-issue-form')) do + find('.form-control').set('bug') + click_button 'Submit issue' + end + + wait_for_vue_resource + + expect(page).to have_selector('.issue-boards-sidebar') + end end context 'unauthorized user' do diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb new file mode 100644 index 00000000000..f160052a844 --- /dev/null +++ b/spec/features/boards/sidebar_spec.rb @@ -0,0 +1,312 @@ +require 'rails_helper' + +describe 'Issue Boards', feature: true, js: true do + include WaitForAjax + include WaitForVueResource + + let(:project) { create(:empty_project, :public) } + let(:board) { create(:board, project: project) } + let(:user) { create(:user) } + let!(:label) { create(:label, project: project) } + let!(:label2) { create(:label, project: project) } + let!(:milestone) { create(:milestone, project: project) } + let!(:issue2) { create(:labeled_issue, project: project, assignee: user, milestone: milestone, labels: [label]) } + let!(:issue) { create(:issue, project: project) } + + before do + project.team << [user, :master] + + login_as(user) + + visit namespace_project_board_path(project.namespace, project, board) + wait_for_vue_resource + end + + it 'shows sidebar when clicking issue' do + page.within(first('.board')) do + first('.card').click + end + + expect(page).to have_selector('.issue-boards-sidebar') + end + + it 'closes sidebar when clicking issue' do + page.within(first('.board')) do + first('.card').click + end + + expect(page).to have_selector('.issue-boards-sidebar') + + page.within(first('.board')) do + first('.card').click + end + + expect(page).not_to have_selector('.issue-boards-sidebar') + end + + it 'closes sidebar when clicking close button' do + page.within(first('.board')) do + first('.card').click + end + + expect(page).to have_selector('.issue-boards-sidebar') + + find('.gutter-toggle').click + + expect(page).not_to have_selector('.issue-boards-sidebar') + end + + it 'shows issue details when sidebar is open' do + page.within(first('.board')) do + first('.card').click + end + + page.within('.issue-boards-sidebar') do + expect(page).to have_content(issue.title) + expect(page).to have_content(issue.to_reference) + end + end + + context 'assignee' do + it 'updates the issues assignee' do + page.within(first('.board')) do + first('.card').click + end + + page.within('.assignee') do + click_link 'Edit' + + wait_for_ajax + + page.within('.dropdown-menu-user') do + click_link user.name + + wait_for_vue_resource + end + + expect(page).to have_content(user.name) + end + + page.within(first('.board')) do + page.within(first('.card')) do + expect(page).to have_selector('.avatar') + end + end + end + + it 'removes the assignee' do + page.within(first('.board')) do + find('.card:nth-child(2)').click + end + + page.within('.assignee') do + click_link 'Edit' + + wait_for_ajax + + page.within('.dropdown-menu-user') do + click_link 'Unassigned' + + wait_for_vue_resource + end + + expect(page).to have_content('No assignee') + end + + page.within(first('.board')) do + page.within(find('.card:nth-child(2)')) do + expect(page).not_to have_selector('.avatar') + end + end + end + + it 'assignees to current user' do + page.within(first('.board')) do + first('.card').click + end + + page.within('.assignee') do + click_link 'assign yourself' + + wait_for_vue_resource + + expect(page).to have_content(user.name) + end + + page.within(first('.board')) do + page.within(first('.card')) do + expect(page).to have_selector('.avatar') + end + end + end + end + + context 'milestone' do + it 'adds a milestone' do + page.within(first('.board')) do + first('.card').click + end + + page.within('.milestone') do + click_link 'Edit' + + wait_for_ajax + + click_link milestone.title + + wait_for_vue_resource + + page.within('.value') do + expect(page).to have_content(milestone.title) + end + end + end + + it 'removes a milestone' do + page.within(first('.board')) do + find('.card:nth-child(2)').click + end + + page.within('.milestone') do + click_link 'Edit' + + wait_for_ajax + + click_link "No Milestone" + + wait_for_vue_resource + + page.within('.value') do + expect(page).not_to have_content(milestone.title) + end + end + end + end + + context 'due date' do + it 'updates due date' do + page.within(first('.board')) do + first('.card').click + end + + page.within('.due_date') do + click_link 'Edit' + + click_link Date.today.day + + wait_for_vue_resource + + expect(page).to have_content(Date.today.to_s(:medium)) + end + end + end + + context 'labels' do + it 'adds a single label' do + page.within(first('.board')) do + first('.card').click + end + + page.within('.labels') do + click_link 'Edit' + + wait_for_ajax + + click_link label.title + + wait_for_vue_resource + + find('.dropdown-menu-close-icon').click + + page.within('.value') do + expect(page).to have_selector('.label', count: 1) + expect(page).to have_content(label.title) + end + end + + page.within(first('.board')) do + page.within(first('.card')) do + expect(page).to have_selector('.label', count: 1) + expect(page).to have_content(label.title) + end + end + end + + it 'adds a multiple labels' do + page.within(first('.board')) do + first('.card').click + end + + page.within('.labels') do + click_link 'Edit' + + wait_for_ajax + + click_link label.title + click_link label2.title + + wait_for_vue_resource + + find('.dropdown-menu-close-icon').click + + page.within('.value') do + expect(page).to have_selector('.label', count: 2) + expect(page).to have_content(label.title) + expect(page).to have_content(label2.title) + end + end + + page.within(first('.board')) do + page.within(first('.card')) do + expect(page).to have_selector('.label', count: 2) + expect(page).to have_content(label.title) + expect(page).to have_content(label2.title) + end + end + end + + it 'removes a label' do + page.within(first('.board')) do + find('.card:nth-child(2)').click + end + + page.within('.labels') do + click_link 'Edit' + + wait_for_ajax + + click_link label.title + + wait_for_vue_resource + + find('.dropdown-menu-close-icon').click + + page.within('.value') do + expect(page).to have_selector('.label', count: 0) + expect(page).not_to have_content(label.title) + end + end + + page.within(first('.board')) do + page.within(find('.card:nth-child(2)')) do + expect(page).not_to have_selector('.label', count: 1) + expect(page).not_to have_content(label.title) + end + end + end + end + + context 'subscription' do + it 'changes issue subscription' do + page.within(first('.board')) do + first('.card').click + end + + page.within('.subscription') do + click_button 'Subscribe' + + expect(page).to have_content("You're receiving notifications because you're subscribed to this thread.") + end + end + end +end diff --git a/spec/features/issues/filter_by_milestone_spec.rb b/spec/features/issues/filter_by_milestone_spec.rb index 485dc560061..88e1549a22b 100644 --- a/spec/features/issues/filter_by_milestone_spec.rb +++ b/spec/features/issues/filter_by_milestone_spec.rb @@ -58,6 +58,22 @@ feature 'Issue filtering by Milestone', feature: true do expect(page).to have_css('.issue', count: 1) end + context 'when milestone has single quotes in title' do + background do + milestone.update(name: "rock 'n' roll") + end + + scenario 'filters by a specific Milestone', js: true do + create(:issue, project: project, milestone: milestone) + create(:issue, project: project) + + visit_issues(project) + filter_by_milestone(milestone.title) + + expect(page).to have_css('.issue', count: 1) + end + end + def visit_issues(project) visit namespace_project_issues_path(project.namespace, project) end diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb index 996f39ea06d..76bcfbe523a 100644 --- a/spec/features/login_spec.rb +++ b/spec/features/login_spec.rb @@ -215,4 +215,69 @@ feature 'Login', feature: true do end end end + + describe 'UI tabs and panes' do + context 'when no defaults are changed' do + it 'correctly renders tabs and panes' do + ensure_tab_pane_correctness + end + end + + context 'when signup is disabled' do + before do + stub_application_setting(signup_enabled: false) + end + + it 'correctly renders tabs and panes' do + ensure_tab_pane_correctness + end + end + + context 'when ldap is enabled' do + before do + visit new_user_session_path + allow(page).to receive(:form_based_providers).and_return([:ldapmain]) + allow(page).to receive(:ldap_enabled).and_return(true) + end + + it 'correctly renders tabs and panes' do + ensure_tab_pane_correctness(false) + end + end + + context 'when crowd is enabled' do + before do + visit new_user_session_path + allow(page).to receive(:form_based_providers).and_return([:crowd]) + allow(page).to receive(:crowd_enabled?).and_return(true) + end + + it 'correctly renders tabs and panes' do + ensure_tab_pane_correctness(false) + end + end + + def ensure_tab_pane_correctness(visit_path = true) + if visit_path + visit new_user_session_path + end + + ensure_tab_pane_counts + ensure_one_active_tab + ensure_one_active_pane + end + + def ensure_tab_pane_counts + tabs_count = page.all('[role="tab"]').size + expect(page).to have_selector('[role="tabpanel"]', count: tabs_count) + end + + def ensure_one_active_tab + expect(page).to have_selector('.nav-tabs > li.active', count: 1) + end + + def ensure_one_active_pane + expect(page).to have_selector('.tab-pane.active', count: 1) + end + end end diff --git a/spec/features/merge_requests/created_from_fork_spec.rb b/spec/features/merge_requests/created_from_fork_spec.rb index cfc1244429f..142649297cc 100644 --- a/spec/features/merge_requests/created_from_fork_spec.rb +++ b/spec/features/merge_requests/created_from_fork_spec.rb @@ -40,8 +40,6 @@ feature 'Merge request created from fork' do end context 'pipeline present in source project' do - include WaitForAjax - given(:pipeline) do create(:ci_pipeline, project: fork_project, @@ -57,7 +55,6 @@ feature 'Merge request created from fork' do scenario 'user visits a pipelines page', js: true do visit_merge_request(merge_request) page.within('.merge-request-tabs') { click_link 'Builds' } - wait_for_ajax page.within('table.ci-table') do expect(page).to have_content 'rspec' diff --git a/spec/features/merge_requests/filter_by_milestone_spec.rb b/spec/features/merge_requests/filter_by_milestone_spec.rb index d917d5950ec..f6e9230c8da 100644 --- a/spec/features/merge_requests/filter_by_milestone_spec.rb +++ b/spec/features/merge_requests/filter_by_milestone_spec.rb @@ -67,6 +67,23 @@ feature 'Merge Request filtering by Milestone', feature: true do expect(page).to have_css('.merge-request', count: 1) end + context 'when milestone has single quotes in title' do + background do + milestone.update(name: "rock 'n' roll") + end + + scenario 'filters by a specific Milestone', js: true do + create(:merge_request, :with_diffs, source_project: project, milestone: milestone) + create(:merge_request, :simple, source_project: project) + + visit_merge_requests(project) + filter_by_milestone(milestone.title) + + expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) + expect(page).to have_css('.merge-request', count: 1) + end + end + def visit_merge_requests(project) visit namespace_project_merge_requests_path(project.namespace, project) end diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb index d886909ce85..2f377312ea5 100644 --- a/spec/features/projects/issuable_templates_spec.rb +++ b/spec/features/projects/issuable_templates_spec.rb @@ -77,7 +77,7 @@ feature 'issuable templates', feature: true, js: true do scenario 'user selects "bug" template' do select_template 'bug' wait_for_ajax - preview_template("#{prior_description}\n\n#{template_content}") + preview_template("#{template_content}") save_changes end end diff --git a/spec/finders/labels_finder_spec.rb b/spec/finders/labels_finder_spec.rb index 27acc464ea2..10cfb66ec1c 100644 --- a/spec/finders/labels_finder_spec.rb +++ b/spec/finders/labels_finder_spec.rb @@ -38,6 +38,14 @@ describe LabelsFinder do expect(finder.execute).to eq [group_label_2, group_label_3, project_label_1, group_label_1, project_label_2, project_label_4] end + + it 'returns labels available if nil title is supplied' do + group_2.add_developer(user) + # params[:title] will return `nil` regardless whether it is specified + finder = described_class.new(user, title: nil) + + expect(finder.execute).to eq [group_label_2, group_label_3, project_label_1, group_label_1, project_label_2, project_label_4] + end end context 'filtering by group_id' do @@ -64,6 +72,30 @@ describe LabelsFinder do expect(finder.execute).to eq [group_label_2] end + + it 'returns label with title alias' do + finder = described_class.new(user, name: 'Group Label 2') + + expect(finder.execute).to eq [group_label_2] + end + + it 'returns no labels if empty title is supplied' do + finder = described_class.new(user, title: []) + + expect(finder.execute).to be_empty + end + + it 'returns no labels if blank title is supplied' do + finder = described_class.new(user, title: '') + + expect(finder.execute).to be_empty + end + + it 'returns no labels if empty name is supplied' do + finder = described_class.new(user, name: []) + + expect(finder.execute).to be_empty + end end end end diff --git a/spec/fixtures/api/schemas/issue.json b/spec/fixtures/api/schemas/issue.json index 532ebb9640e..77f2bcee1f3 100644 --- a/spec/fixtures/api/schemas/issue.json +++ b/spec/fixtures/api/schemas/issue.json @@ -9,6 +9,7 @@ "iid": { "type": "integer" }, "title": { "type": "string" }, "confidential": { "type": "boolean" }, + "due_date": { "type": ["date", "null"] }, "labels": { "type": "array", "items": { @@ -42,7 +43,8 @@ "name": { "type": "string" }, "username": { "type": "string" }, "avatar_url": { "type": "uri" } - } + }, + "subscribed": { "type": ["boolean", "null"] } }, "additionalProperties": false } diff --git a/spec/javascripts/activities_spec.js.es6 b/spec/javascripts/activities_spec.js.es6 index 743b15460c6..6699dee3cf7 100644 --- a/spec/javascripts/activities_spec.js.es6 +++ b/spec/javascripts/activities_spec.js.es6 @@ -1,4 +1,4 @@ -/*= require jquery.cookie.js */ +/*= require js.cookie.js */ /*= require jquery.endless-scroll.js */ /*= require pager */ /*= require activities */ diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index 019ce3b0702..f5e400a2e39 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -1,7 +1,7 @@ /*= require awards_handler */ /*= require jquery */ -/*= require jquery.cookie */ +/*= require js.cookie */ /*= require ./fixtures/emoji_menu */ (function() { @@ -44,7 +44,6 @@ spyOn(jQuery, 'get').and.callFake(function(req, cb) { return cb(window.emojiMenu); }); - spyOn(jQuery, 'cookie'); }); afterEach(function() { // restore original url root value @@ -190,28 +189,6 @@ return expect($thumbsUpEmoji.data("original-title")).toBe('sam'); }); }); - describe('::addEmojiToFrequentlyUsedList', function() { - it('should set a cookie with the correct default path', function() { - gon.relative_url_root = ''; - awardsHandler.addEmojiToFrequentlyUsedList('sunglasses'); - expect(jQuery.cookie) - .toHaveBeenCalledWith('frequently_used_emojis', 'sunglasses', { - path: '/', - expires: 365 - }) - ; - }); - it('should set a cookie with the correct custom root path', function() { - gon.relative_url_root = '/gitlab/subdir'; - awardsHandler.addEmojiToFrequentlyUsedList('alien'); - expect(jQuery.cookie) - .toHaveBeenCalledWith('frequently_used_emojis', 'alien', { - path: '/gitlab/subdir', - expires: 365 - }) - ; - }); - }); describe('search', function() { return it('should filter the emoji', function() { $('.js-add-award').eq(0).click(); diff --git a/spec/javascripts/boards/boards_store_spec.js.es6 b/spec/javascripts/boards/boards_store_spec.js.es6 index 15c305ce321..8402a996192 100644 --- a/spec/javascripts/boards/boards_store_spec.js.es6 +++ b/spec/javascripts/boards/boards_store_spec.js.es6 @@ -1,6 +1,6 @@ //= require jquery //= require jquery_ujs -//= require jquery.cookie +//= require js.cookie //= require vue //= require vue-resource //= require lib/utils/url_utility @@ -17,7 +17,7 @@ gl.boardService = new BoardService('/test/issue-boards/board', '1'); gl.issueBoards.BoardsStore.create(); - $.cookie('issue_board_welcome_hidden', 'false'); + Cookies.set('issue_board_welcome_hidden', 'false'); }); describe('Store', () => { diff --git a/spec/javascripts/boards/issue_spec.js.es6 b/spec/javascripts/boards/issue_spec.js.es6 index 328c6f82ab5..cfece7ea7e8 100644 --- a/spec/javascripts/boards/issue_spec.js.es6 +++ b/spec/javascripts/boards/issue_spec.js.es6 @@ -1,6 +1,6 @@ //= require jquery //= require jquery_ujs -//= require jquery.cookie +//= require js.cookie //= require vue //= require vue-resource //= require lib/utils/url_utility diff --git a/spec/javascripts/boards/list_spec.js.es6 b/spec/javascripts/boards/list_spec.js.es6 index ec78d82e919..7b2c0945ce8 100644 --- a/spec/javascripts/boards/list_spec.js.es6 +++ b/spec/javascripts/boards/list_spec.js.es6 @@ -1,6 +1,6 @@ //= require jquery //= require jquery_ujs -//= require jquery.cookie +//= require js.cookie //= require vue //= require vue-resource //= require lib/utils/url_utility diff --git a/spec/javascripts/gl_field_errors_spec.js.es6 b/spec/javascripts/gl_field_errors_spec.js.es6 index 36feb2b2aa5..da9259edd78 100644 --- a/spec/javascripts/gl_field_errors_spec.js.es6 +++ b/spec/javascripts/gl_field_errors_spec.js.es6 @@ -11,12 +11,12 @@ this.fieldErrors = new global.GlFieldErrors($form); }); - it('should properly initialize the form', function() { + it('should select the correct input elements', function() { expect(this.$form).toBeDefined(); expect(this.$form.length).toBe(1); expect(this.fieldErrors).toBeDefined(); const inputs = this.fieldErrors.state.inputs; - expect(inputs.length).toBe(5); + expect(inputs.length).toBe(4); }); it('should ignore elements with custom error handling', function() { diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js index c937a4706f7..5aa8d5b5826 100644 --- a/spec/javascripts/right_sidebar_spec.js +++ b/spec/javascripts/right_sidebar_spec.js @@ -1,7 +1,7 @@ /*= require right_sidebar */ /*= require jquery */ -/*= require jquery.cookie */ +/*= require js.cookie */ (function() { var $aside, $icon, $labelsIcon, $page, $toggle, assertSidebarState; diff --git a/spec/lib/banzai/filter/relative_link_filter_spec.rb b/spec/lib/banzai/filter/relative_link_filter_spec.rb index 6b58f3e43ee..2bfa51deb20 100644 --- a/spec/lib/banzai/filter/relative_link_filter_spec.rb +++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb @@ -50,14 +50,6 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do end end - shared_examples :relative_to_requested do - it 'rebuilds URL relative to the requested path' do - doc = filter(link('users.md')) - expect(doc.at_css('a')['href']). - to eq "/#{project_path}/blob/#{ref}/doc/api/users.md" - end - end - context 'with a project_wiki' do let(:project_wiki) { double('ProjectWiki') } include_examples :preserve_unchanged @@ -188,12 +180,38 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do context 'when requested path is a file in the repo' do let(:requested_path) { 'doc/api/README.md' } - include_examples :relative_to_requested + it 'rebuilds URL relative to the containing directory' do + doc = filter(link('users.md')) + expect(doc.at_css('a')['href']).to eq "/#{project_path}/blob/#{Addressable::URI.escape(ref)}/doc/api/users.md" + end end context 'when requested path is a directory in the repo' do - let(:requested_path) { 'doc/api' } - include_examples :relative_to_requested + let(:requested_path) { 'doc/api/' } + it 'rebuilds URL relative to the directory' do + doc = filter(link('users.md')) + expect(doc.at_css('a')['href']).to eq "/#{project_path}/blob/#{Addressable::URI.escape(ref)}/doc/api/users.md" + end + end + + context 'when ref name contains percent sign' do + let(:ref) { '100%branch' } + let(:commit) { project.commit('1b12f15a11fc6e62177bef08f47bc7b5ce50b141') } + let(:requested_path) { 'foo/bar/' } + it 'correctly escapes the ref' do + doc = filter(link('.gitkeep')) + expect(doc.at_css('a')['href']).to eq "/#{project_path}/blob/#{Addressable::URI.escape(ref)}/foo/bar/.gitkeep" + end + end + + context 'when requested path is a directory with space in the repo' do + let(:ref) { 'master' } + let(:commit) { project.commit('38008cb17ce1466d8fec2dfa6f6ab8dcfe5cf49e') } + let(:requested_path) { 'with space/' } + it 'does not escape the space twice' do + doc = filter(link('README.md')) + expect(doc.at_css('a')['href']).to eq "/#{project_path}/blob/#{Addressable::URI.escape(ref)}/with%20space/README.md" + end end end diff --git a/spec/lib/constraints/namespace_url_constrainer_spec.rb b/spec/lib/constraints/namespace_url_constrainer_spec.rb index a5feaacb8ee..7814711fe27 100644 --- a/spec/lib/constraints/namespace_url_constrainer_spec.rb +++ b/spec/lib/constraints/namespace_url_constrainer_spec.rb @@ -17,6 +17,16 @@ describe NamespaceUrlConstrainer, lib: true do it { expect(subject.matches?(request '/g/gitlab')).to be_falsey } it { expect(subject.matches?(request '/.gitlab')).to be_falsey } end + + context 'relative url' do + before do + allow(Gitlab::Application.config).to receive(:relative_url_root) { '/gitlab' } + end + + it { expect(subject.matches?(request '/gitlab/gitlab')).to be_truthy } + it { expect(subject.matches?(request '/gitlab/gitlab-ce')).to be_falsey } + it { expect(subject.matches?(request '/gitlab/')).to be_falsey } + end end def request(path) diff --git a/spec/lib/gitlab/exclusive_lease_spec.rb b/spec/lib/gitlab/exclusive_lease_spec.rb index fbdb7ea34ac..6b3bd08b978 100644 --- a/spec/lib/gitlab/exclusive_lease_spec.rb +++ b/spec/lib/gitlab/exclusive_lease_spec.rb @@ -1,21 +1,36 @@ require 'spec_helper' -describe Gitlab::ExclusiveLease do - it 'cannot obtain twice before the lease has expired' do - lease = Gitlab::ExclusiveLease.new(unique_key, timeout: 3600) - expect(lease.try_obtain).to eq(true) - expect(lease.try_obtain).to eq(false) - end +describe Gitlab::ExclusiveLease, type: :redis do + let(:unique_key) { SecureRandom.hex(10) } + + describe '#try_obtain' do + it 'cannot obtain twice before the lease has expired' do + lease = Gitlab::ExclusiveLease.new(unique_key, timeout: 3600) + expect(lease.try_obtain).to eq(true) + expect(lease.try_obtain).to eq(false) + end - it 'can obtain after the lease has expired' do - timeout = 1 - lease = Gitlab::ExclusiveLease.new(unique_key, timeout: timeout) - lease.try_obtain # start the lease - sleep(2 * timeout) # lease should have expired now - expect(lease.try_obtain).to eq(true) + it 'can obtain after the lease has expired' do + timeout = 1 + lease = Gitlab::ExclusiveLease.new(unique_key, timeout: timeout) + lease.try_obtain # start the lease + sleep(2 * timeout) # lease should have expired now + expect(lease.try_obtain).to eq(true) + end end - def unique_key - SecureRandom.hex(10) + describe '#exists?' do + it 'returns true for an existing lease' do + lease = Gitlab::ExclusiveLease.new(unique_key, timeout: 3600) + lease.try_obtain + + expect(lease.exists?).to eq(true) + end + + it 'returns false for a lease that does not exist' do + lease = Gitlab::ExclusiveLease.new(unique_key, timeout: 3600) + + expect(lease.exists?).to eq(false) + end end end diff --git a/spec/mailers/emails/builds_spec.rb b/spec/mailers/emails/builds_spec.rb index 0df89938e97..d968096783c 100644 --- a/spec/mailers/emails/builds_spec.rb +++ b/spec/mailers/emails/builds_spec.rb @@ -1,6 +1,5 @@ require 'spec_helper' require 'email_spec' -require 'mailers/shared/notify' describe Notify do include EmailSpec::Matchers diff --git a/spec/mailers/emails/merge_requests_spec.rb b/spec/mailers/emails/merge_requests_spec.rb index 4d3811af254..e22858d1d8f 100644 --- a/spec/mailers/emails/merge_requests_spec.rb +++ b/spec/mailers/emails/merge_requests_spec.rb @@ -1,6 +1,5 @@ require 'spec_helper' require 'email_spec' -require 'mailers/shared/notify' describe Notify, "merge request notifications" do include EmailSpec::Matchers diff --git a/spec/mailers/emails/profile_spec.rb b/spec/mailers/emails/profile_spec.rb index 781472d0c00..14bc062ef12 100644 --- a/spec/mailers/emails/profile_spec.rb +++ b/spec/mailers/emails/profile_spec.rb @@ -1,6 +1,5 @@ require 'spec_helper' require 'email_spec' -require 'mailers/shared/notify' describe Notify do include EmailSpec::Matchers diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index c8207e58e90..f5f3f58613d 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -1,6 +1,5 @@ require 'spec_helper' require 'email_spec' -require 'mailers/shared/notify' describe Notify do include EmailSpec::Helpers diff --git a/spec/models/email_spec.rb b/spec/models/email_spec.rb index d9df9e0f907..fe4de1b2afb 100644 --- a/spec/models/email_spec.rb +++ b/spec/models/email_spec.rb @@ -6,4 +6,9 @@ describe Email, models: true do subject { build(:email) } end end + + it 'normalize email value' do + expect(described_class.new(email: ' inFO@exAMPLe.com ').email) + .to eq 'info@example.com' + end end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 47f89f744cb..ac862055ebc 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -265,10 +265,4 @@ describe Group, models: true do members end - - describe '#web_url' do - it 'returns the canonical URL' do - expect(group.web_url).to include("groups/#{group.name}") - end - end end diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb index b2fe96e2e02..f6b2ec5ae31 100644 --- a/spec/models/members/project_member_spec.rb +++ b/spec/models/members/project_member_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe ProjectMember, models: true do describe 'associations' do - it { is_expected.to belong_to(:project).class_name('Project').with_foreign_key(:source_id) } + it { is_expected.to belong_to(:project).with_foreign_key(:source_id) } end describe 'validations' do diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 6e5137602aa..1067ff7bb4d 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -6,8 +6,8 @@ describe MergeRequest, models: true do subject { create(:merge_request) } describe 'associations' do - it { is_expected.to belong_to(:target_project).with_foreign_key(:target_project_id).class_name('Project') } - it { is_expected.to belong_to(:source_project).with_foreign_key(:source_project_id).class_name('Project') } + it { is_expected.to belong_to(:target_project).class_name('Project') } + it { is_expected.to belong_to(:source_project).class_name('Project') } it { is_expected.to belong_to(:merge_user).class_name("User") } it { is_expected.to have_many(:merge_request_diffs).dependent(:destroy) } end @@ -1286,7 +1286,8 @@ describe MergeRequest, models: true do let(:project) { create(:project) } let(:user) { create(:user) } let(:fork_project) { create(:project, forked_from_project: project, namespace: user.namespace) } - let(:merge_request) do + + let!(:merge_request) do create(:closed_merge_request, source_project: fork_project, target_project: project) diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index f977cf73673..187a1bf2d79 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1146,28 +1146,17 @@ describe Repository, models: true do end describe '#before_import' do - it 'flushes the emptiness cachess' do - expect(repository).to receive(:expire_emptiness_caches) - - repository.before_import - end - - it 'flushes the exists cache' do - expect(repository).to receive(:expire_exists_cache) + it 'flushes the repository caches' do + expect(repository).to receive(:expire_content_cache) repository.before_import end end describe '#after_import' do - it 'flushes the emptiness cachess' do - expect(repository).to receive(:expire_emptiness_caches) - - repository.after_import - end - - it 'flushes the exists cache' do - expect(repository).to receive(:expire_exists_cache) + it 'flushes and builds the cache' do + expect(repository).to receive(:expire_content_cache) + expect(repository).to receive(:build_cache) repository.after_import end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 65b2896930a..10c39b90212 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -15,11 +15,11 @@ describe User, models: true do describe 'associations' do it { is_expected.to have_one(:namespace) } - it { is_expected.to have_many(:snippets).class_name('Snippet').dependent(:destroy) } + it { is_expected.to have_many(:snippets).dependent(:destroy) } it { is_expected.to have_many(:project_members).dependent(:destroy) } it { is_expected.to have_many(:groups) } it { is_expected.to have_many(:keys).dependent(:destroy) } - it { is_expected.to have_many(:events).class_name('Event').dependent(:destroy) } + it { is_expected.to have_many(:events).dependent(:destroy) } it { is_expected.to have_many(:recent_events).class_name('Event') } it { is_expected.to have_many(:issues).dependent(:destroy) } it { is_expected.to have_many(:notes).dependent(:destroy) } diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb index 3fd989dd7a6..905f762d578 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -48,92 +48,154 @@ describe API::API, api: true do end describe 'PUT /projects/:id/repository/branches/:branch/protect' do - it 'protects a single branch' do - put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user) + context "when a protected branch doesn't already exist" do + it 'protects a single branch' do + put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user) - expect(response).to have_http_status(200) - expect(json_response['name']).to eq(branch_name) - expect(json_response['commit']['id']).to eq(branch_sha) - expect(json_response['protected']).to eq(true) - expect(json_response['developers_can_push']).to eq(false) - expect(json_response['developers_can_merge']).to eq(false) - end + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(branch_name) + expect(json_response['commit']['id']).to eq(branch_sha) + expect(json_response['protected']).to eq(true) + expect(json_response['developers_can_push']).to eq(false) + expect(json_response['developers_can_merge']).to eq(false) + end - it 'protects a single branch and developers can push' do - put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), - developers_can_push: true + it 'protects a single branch and developers can push' do + put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), + developers_can_push: true - expect(response).to have_http_status(200) - expect(json_response['name']).to eq(branch_name) - expect(json_response['commit']['id']).to eq(branch_sha) - expect(json_response['protected']).to eq(true) - expect(json_response['developers_can_push']).to eq(true) - expect(json_response['developers_can_merge']).to eq(false) - end + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(branch_name) + expect(json_response['commit']['id']).to eq(branch_sha) + expect(json_response['protected']).to eq(true) + expect(json_response['developers_can_push']).to eq(true) + expect(json_response['developers_can_merge']).to eq(false) + end - it 'protects a single branch and developers can merge' do - put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), - developers_can_merge: true + it 'protects a single branch and developers can merge' do + put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), + developers_can_merge: true - expect(response).to have_http_status(200) - expect(json_response['name']).to eq(branch_name) - expect(json_response['commit']['id']).to eq(branch_sha) - expect(json_response['protected']).to eq(true) - expect(json_response['developers_can_push']).to eq(false) - expect(json_response['developers_can_merge']).to eq(true) - end + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(branch_name) + expect(json_response['commit']['id']).to eq(branch_sha) + expect(json_response['protected']).to eq(true) + expect(json_response['developers_can_push']).to eq(false) + expect(json_response['developers_can_merge']).to eq(true) + end - it 'protects a single branch and developers can push and merge' do - put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), - developers_can_push: true, developers_can_merge: true + it 'protects a single branch and developers can push and merge' do + put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), + developers_can_push: true, developers_can_merge: true - expect(response).to have_http_status(200) - expect(json_response['name']).to eq(branch_name) - expect(json_response['commit']['id']).to eq(branch_sha) - expect(json_response['protected']).to eq(true) - expect(json_response['developers_can_push']).to eq(true) - expect(json_response['developers_can_merge']).to eq(true) - end + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(branch_name) + expect(json_response['commit']['id']).to eq(branch_sha) + expect(json_response['protected']).to eq(true) + expect(json_response['developers_can_push']).to eq(true) + expect(json_response['developers_can_merge']).to eq(true) + end - it 'protects a single branch and developers cannot push and merge' do - put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), - developers_can_push: 'tru', developers_can_merge: 'tr' + it 'protects a single branch and developers cannot push and merge' do + put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), + developers_can_push: 'tru', developers_can_merge: 'tr' - expect(response).to have_http_status(200) - expect(json_response['name']).to eq(branch_name) - expect(json_response['commit']['id']).to eq(branch_sha) - expect(json_response['protected']).to eq(true) - expect(json_response['developers_can_push']).to eq(false) - expect(json_response['developers_can_merge']).to eq(false) + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(branch_name) + expect(json_response['commit']['id']).to eq(branch_sha) + expect(json_response['protected']).to eq(true) + expect(json_response['developers_can_push']).to eq(false) + expect(json_response['developers_can_merge']).to eq(false) + end end - context 'on a protected branch' do - let(:protected_branch) { 'foo' } - + context 'for an existing protected branch' do before do - project.repository.add_branch(user, protected_branch, 'master') - create(:protected_branch, :developers_can_push, :developers_can_merge, project: project, name: protected_branch) + project.repository.add_branch(user, protected_branch.name, 'master') end - it 'updates that a developer can push' do - put api("/projects/#{project.id}/repository/branches/#{protected_branch}/protect", user), - developers_can_push: false, developers_can_merge: false + context "when developers can push and merge" do + let(:protected_branch) { create(:protected_branch, :developers_can_push, :developers_can_merge, project: project, name: 'protected_branch') } + + it 'updates that a developer cannot push or merge' do + put api("/projects/#{project.id}/repository/branches/#{protected_branch.name}/protect", user), + developers_can_push: false, developers_can_merge: false + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(protected_branch.name) + expect(json_response['protected']).to eq(true) + expect(json_response['developers_can_push']).to eq(false) + expect(json_response['developers_can_merge']).to eq(false) + end + + it "doesn't result in 0 access levels when 'developers_can_push' is switched off" do + put api("/projects/#{project.id}/repository/branches/#{protected_branch.name}/protect", user), + developers_can_push: false + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(protected_branch.name) + expect(protected_branch.reload.push_access_levels.first).to be_present + expect(protected_branch.reload.push_access_levels.first.access_level).to eq(Gitlab::Access::MASTER) + end + + it "doesn't result in 0 access levels when 'developers_can_merge' is switched off" do + put api("/projects/#{project.id}/repository/branches/#{protected_branch.name}/protect", user), + developers_can_merge: false + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(protected_branch.name) + expect(protected_branch.reload.merge_access_levels.first).to be_present + expect(protected_branch.reload.merge_access_levels.first.access_level).to eq(Gitlab::Access::MASTER) + end + end + + context "when developers cannot push or merge" do + let(:protected_branch) { create(:protected_branch, project: project, name: 'protected_branch') } + + it 'updates that a developer can push and merge' do + put api("/projects/#{project.id}/repository/branches/#{protected_branch.name}/protect", user), + developers_can_push: true, developers_can_merge: true + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(protected_branch.name) + expect(json_response['protected']).to eq(true) + expect(json_response['developers_can_push']).to eq(true) + expect(json_response['developers_can_merge']).to eq(true) + end + end + end + + context "multiple API calls" do + it "returns success when `protect` is called twice" do + put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user) + put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user) expect(response).to have_http_status(200) - expect(json_response['name']).to eq(protected_branch) + expect(json_response['name']).to eq(branch_name) expect(json_response['protected']).to eq(true) expect(json_response['developers_can_push']).to eq(false) expect(json_response['developers_can_merge']).to eq(false) end - it 'does not update that a developer can push' do - put api("/projects/#{project.id}/repository/branches/#{protected_branch}/protect", user), - developers_can_push: 'foobar', developers_can_merge: 'foo' + it "returns success when `protect` is called twice with `developers_can_push` turned on" do + put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), developers_can_push: true + put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), developers_can_push: true expect(response).to have_http_status(200) - expect(json_response['name']).to eq(protected_branch) + expect(json_response['name']).to eq(branch_name) expect(json_response['protected']).to eq(true) expect(json_response['developers_can_push']).to eq(true) + expect(json_response['developers_can_merge']).to eq(false) + end + + it "returns success when `protect` is called twice with `developers_can_merge` turned on" do + put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), developers_can_merge: true + put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), developers_can_merge: true + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(branch_name) + expect(json_response['protected']).to eq(true) + expect(json_response['developers_can_push']).to eq(false) expect(json_response['developers_can_merge']).to eq(true) end end @@ -147,12 +209,6 @@ describe API::API, api: true do put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user2) expect(response).to have_http_status(403) end - - it "returns success when protect branch again" do - put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user) - put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user) - expect(response).to have_http_status(200) - end end describe "PUT /projects/:id/repository/branches/:branch/unprotect" do diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 66fa0c0c01f..a6e8550fac3 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -72,6 +72,17 @@ describe API::API, api: true do expect(json_response['message']).to include "\"since\" must be a timestamp in ISO 8601 format" end end + + context "path optional parameter" do + it "returns project commits matching provided path parameter" do + path = 'files/ruby/popen.rb' + + get api("/projects/#{project.id}/repository/commits?path=#{path}", user) + + expect(json_response.size).to eq(3) + expect(json_response.first["id"]).to eq("570e7b2abdd848b95f2f578043fc23bd6f6fd24d") + end + end end describe "Create a commit with multiple files and actions" do diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb index 7d8cc45327c..65897edba7f 100644 --- a/spec/requests/api/deploy_keys_spec.rb +++ b/spec/requests/api/deploy_keys_spec.rb @@ -6,6 +6,7 @@ describe API::API, api: true do let(:user) { create(:user) } let(:admin) { create(:admin) } let(:project) { create(:project, creator_id: user.id) } + let(:project2) { create(:project, creator_id: user.id) } let(:deploy_key) { create(:deploy_key, public: true) } let!(:deploy_keys_project) do @@ -96,6 +97,22 @@ describe API::API, api: true do post api("/projects/#{project.id}/deploy_keys", admin), key_attrs end.to change{ project.deploy_keys.count }.by(1) end + + it 'returns an existing ssh key when attempting to add a duplicate' do + expect do + post api("/projects/#{project.id}/deploy_keys", admin), { key: deploy_key.key, title: deploy_key.title } + end.not_to change { project.deploy_keys.count } + + expect(response).to have_http_status(201) + end + + it 'joins an existing ssh key to a new project' do + expect do + post api("/projects/#{project2.id}/deploy_keys", admin), { key: deploy_key.key, title: deploy_key.title } + end.to change { project2.deploy_keys.count }.by(1) + + expect(response).to have_http_status(201) + end end describe 'DELETE /projects/:id/deploy_keys/:key_id' do diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb index 867bc615b97..46641fcd846 100644 --- a/spec/requests/api/labels_spec.rb +++ b/spec/requests/api/labels_spec.rb @@ -159,14 +159,14 @@ describe API::API, api: true do it 'returns 400 if no label name given' do put api("/projects/#{project.id}/labels", user), new_name: 'label2' expect(response).to have_http_status(400) - expect(json_response['message']).to eq('400 (Bad request) "name" not given') + expect(json_response['error']).to eq('name is missing') end it 'returns 400 if no new parameters given' do put api("/projects/#{project.id}/labels", user), name: 'label1' expect(response).to have_http_status(400) - expect(json_response['message']).to eq('Required parameters '\ - '"new_name" or "color" missing') + expect(json_response['error']).to eq('new_name, color, description are missing, '\ + 'at least one parameter must be provided') end it 'returns 400 for invalid name' do diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index d58bedc3bf7..0124b7271b3 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -221,12 +221,23 @@ describe API::API, api: true do end end - context 'when the user is posting an award emoji' do + context 'when the user is posting an award emoji on an issue created by someone else' do + let(:issue2) { create(:issue, project: project) } + it 'returns an award emoji' do + post api("/projects/#{project.id}/issues/#{issue2.id}/notes", user), body: ':+1:' + + expect(response).to have_http_status(201) + expect(json_response['awardable_id']).to eq issue2.id + end + end + + context 'when the user is posting an award emoji on his/her own issue' do + it 'creates a new issue note' do post api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: ':+1:' expect(response).to have_http_status(201) - expect(json_response['awardable_id']).to eq issue.id + expect(json_response['body']).to eq(':+1:') end end end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index d48752473f3..ae8639d78d5 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -958,6 +958,29 @@ describe API::API, api: true do expect(joined_event['author']['name']).to eq(user.name) end end + + context 'when there are multiple events from different projects' do + let(:second_note) { create(:note_on_issue, project: create(:empty_project)) } + let(:third_note) { create(:note_on_issue, project: project) } + + before do + second_note.project.add_user(user, :developer) + + [second_note, third_note].each do |note| + EventCreateService.new.leave_note(note, user) + end + end + + it 'returns events in the correct order (from newest to oldest)' do + get api("/users/#{user.id}/events", user) + + comment_events = json_response.select { |e| e['action_name'] == 'commented on' } + + expect(comment_events[0]['target_id']).to eq(third_note.id) + expect(comment_events[1]['target_id']).to eq(second_note.id) + expect(comment_events[2]['target_id']).to eq(note.id) + end + end end it 'returns a 404 error if not found' do diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index 93bf0f64963..f0ded06b785 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -23,14 +23,15 @@ describe Issues::MoveService, services: true do old_project.team << [user, :reporter] new_project.team << [user, :reporter] - ['label1', 'label2'].each do |label| + labels = Array.new(2) { |x| "label%d" % (x + 1) } + + labels.each do |label| old_issue.labels << create(:label, project_id: old_project.id, title: label) - end - new_project.labels << create(:label, title: 'label1') - new_project.labels << create(:label, title: 'label2') + new_project.labels << create(:label, title: label) + end end end @@ -207,10 +208,10 @@ describe Issues::MoveService, services: true do end end - describe 'rewritting references' do + describe 'rewriting references' do include_context 'issue move executed' - context 'issue reference' do + context 'issue references' do let(:another_issue) { create(:issue, project: old_project) } let(:description) { "Some description #{another_issue.to_reference}" } @@ -219,6 +220,16 @@ describe Issues::MoveService, services: true do .to eq "Some description #{old_project.to_reference}#{another_issue.to_reference}" end end + + context "user references" do + let(:another_issue) { create(:issue, project: old_project) } + let(:description) { "Some description #{user.to_reference}" } + + it "doesn't throw any errors for issues containing user references" do + expect(new_issue.description) + .to eq "Some description #{user.to_reference}" + end + end end context 'moving to same project' do @@ -277,5 +288,25 @@ describe Issues::MoveService, services: true do it { expect { move }.to raise_error(StandardError, /permissions/) } end end + + context 'movable issue with no assigned labels' do + before do + old_project.team << [user, :reporter] + new_project.team << [user, :reporter] + + labels = Array.new(2) { |x| "label%d" % (x + 1) } + + labels.each do |label| + new_project.labels << create(:label, title: label) + end + end + + include_context 'issue move executed' + + it 'does not assign labels to new issue' do + expected_label_titles = new_issue.reload.labels.map(&:title) + expect(expected_label_titles.size).to eq 0 + end + end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index b19f5824236..06d52f0f735 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -50,6 +50,12 @@ RSpec.configure do |config| example.run Rails.cache = caching_store end + + config.around(:each, :redis) do |example| + Gitlab::Redis.with(&:flushall) + example.run + Gitlab::Redis.with(&:flushall) + end end FactoryGirl::SyntaxRunner.class_eval do diff --git a/spec/mailers/shared/notify.rb b/spec/support/notify_shared_examples.rb index 3956d05060b..3956d05060b 100644 --- a/spec/mailers/shared/notify.rb +++ b/spec/support/notify_shared_examples.rb diff --git a/spec/support/select2_helper.rb b/spec/support/select2_helper.rb index 35cc51725c6..d30cc8ff9f2 100644 --- a/spec/support/select2_helper.rb +++ b/spec/support/select2_helper.rb @@ -17,9 +17,9 @@ module Select2Helper selector = options.fetch(:from) if options[:multiple] - execute_script("$('#{selector}').select2('val', ['#{value}'], true);") + execute_script("$('#{selector}').select2('val', ['#{value}']).trigger('change');") else - execute_script("$('#{selector}').select2('val', '#{value}', true);") + execute_script("$('#{selector}').select2('val', '#{value}').trigger('change');") end end end diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index 548e7780c36..73bc8326f02 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -9,6 +9,7 @@ describe 'gitlab:app namespace rake task' do Rake.application.rake_require 'tasks/gitlab/backup' Rake.application.rake_require 'tasks/gitlab/shell' Rake.application.rake_require 'tasks/gitlab/db' + Rake.application.rake_require 'tasks/cache' # empty task as env is already loaded Rake::Task.define_task :environment diff --git a/spec/views/projects/merge_requests/_commits.html.haml_spec.rb b/spec/views/projects/merge_requests/_commits.html.haml_spec.rb new file mode 100644 index 00000000000..6f70b3daf8e --- /dev/null +++ b/spec/views/projects/merge_requests/_commits.html.haml_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +describe 'projects/merge_requests/show/_commits.html.haml' do + include Devise::Test::ControllerHelpers + + let(:user) { create(:user) } + let(:target_project) { create(:project) } + + let(:source_project) do + create(:project, forked_from_project: target_project) + end + + let(:merge_request) do + create(:merge_request, :simple, + source_project: source_project, + target_project: target_project, + author: user) + end + + before do + controller.prepend_view_path('app/views/projects') + + assign(:merge_request, merge_request) + assign(:commits, merge_request.commits) + end + + it 'shows commits from source project' do + render + + commit = source_project.commit(merge_request.source_branch) + href = namespace_project_commit_path( + source_project.namespace, + source_project, + commit) + + expect(rendered).to have_link(Commit.truncate_sha(commit.sha), href: href) + end +end diff --git a/spec/workers/project_cache_worker_spec.rb b/spec/workers/project_cache_worker_spec.rb index f5b60b90d11..bfa8c0ff2c6 100644 --- a/spec/workers/project_cache_worker_spec.rb +++ b/spec/workers/project_cache_worker_spec.rb @@ -5,6 +5,26 @@ describe ProjectCacheWorker do subject { described_class.new } + describe '.perform_async' do + it 'schedules the job when no lease exists' do + allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:exists?). + and_return(false) + + expect_any_instance_of(described_class).to receive(:perform) + + described_class.perform_async(project.id) + end + + it 'does not schedule the job when a lease exists' do + allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:exists?). + and_return(true) + + expect_any_instance_of(described_class).not_to receive(:perform) + + described_class.perform_async(project.id) + end + end + describe '#perform' do context 'when an exclusive lease can be obtained' do before do diff --git a/vendor/assets/javascripts/jquery.cookie.js b/vendor/assets/javascripts/jquery.cookie.js deleted file mode 100644 index 6a3e394b403..00000000000 --- a/vendor/assets/javascripts/jquery.cookie.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * jQuery Cookie plugin - * - * Copyright (c) 2010 Klaus Hartl (stilbuero.de) - * Dual licensed under the MIT and GPL licenses: - * http://www.opensource.org/licenses/mit-license.php - * http://www.gnu.org/licenses/gpl.html - * - */ -jQuery.cookie = function (key, value, options) { - - // key and at least value given, set cookie... - if (arguments.length > 1 && String(value) !== "[object Object]") { - options = jQuery.extend({}, options); - - if (value === null || value === undefined) { - options.expires = -1; - } - - if (typeof options.expires === 'number') { - var days = options.expires, t = options.expires = new Date(); - t.setDate(t.getDate() + days); - } - - value = String(value); - - return (document.cookie = [ - encodeURIComponent(key), '=', - options.raw ? value : encodeURIComponent(value), - options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE - options.path ? '; path=' + options.path : '', - options.domain ? '; domain=' + options.domain : '', - options.secure ? '; secure' : '' - ].join('')); - } - - // key and possibly options given, get cookie... - options = value || {}; - var result, decode = options.raw ? function (s) { return s; } : decodeURIComponent; - return (result = new RegExp('(?:^|; )' + encodeURIComponent(key) + '=([^;]*)').exec(document.cookie)) ? decode(result[1]) : null; -}; diff --git a/vendor/assets/javascripts/js.cookie.js b/vendor/assets/javascripts/js.cookie.js new file mode 100644 index 00000000000..92dbba162c4 --- /dev/null +++ b/vendor/assets/javascripts/js.cookie.js @@ -0,0 +1,156 @@ +/*! + * JavaScript Cookie v2.1.3 + * https://github.com/js-cookie/js-cookie + * + * Copyright 2006, 2015 Klaus Hartl & Fagner Brack + * Released under the MIT license + */ +;(function (factory) { + var registeredInModuleLoader = false; + if (typeof define === 'function' && define.amd) { + define(factory); + registeredInModuleLoader = true; + } + if (typeof exports === 'object') { + module.exports = factory(); + registeredInModuleLoader = true; + } + if (!registeredInModuleLoader) { + var OldCookies = window.Cookies; + var api = window.Cookies = factory(); + api.noConflict = function () { + window.Cookies = OldCookies; + return api; + }; + } +}(function () { + function extend () { + var i = 0; + var result = {}; + for (; i < arguments.length; i++) { + var attributes = arguments[ i ]; + for (var key in attributes) { + result[key] = attributes[key]; + } + } + return result; + } + + function init (converter) { + function api (key, value, attributes) { + var result; + if (typeof document === 'undefined') { + return; + } + + // Write + + if (arguments.length > 1) { + attributes = extend({ + path: '/' + }, api.defaults, attributes); + + if (typeof attributes.expires === 'number') { + var expires = new Date(); + expires.setMilliseconds(expires.getMilliseconds() + attributes.expires * 864e+5); + attributes.expires = expires; + } + + try { + result = JSON.stringify(value); + if (/^[\{\[]/.test(result)) { + value = result; + } + } catch (e) {} + + if (!converter.write) { + value = encodeURIComponent(String(value)) + .replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g, decodeURIComponent); + } else { + value = converter.write(value, key); + } + + key = encodeURIComponent(String(key)); + key = key.replace(/%(23|24|26|2B|5E|60|7C)/g, decodeURIComponent); + key = key.replace(/[\(\)]/g, escape); + + return (document.cookie = [ + key, '=', value, + attributes.expires ? '; expires=' + attributes.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE + attributes.path ? '; path=' + attributes.path : '', + attributes.domain ? '; domain=' + attributes.domain : '', + attributes.secure ? '; secure' : '' + ].join('')); + } + + // Read + + if (!key) { + result = {}; + } + + // To prevent the for loop in the first place assign an empty array + // in case there are no cookies at all. Also prevents odd result when + // calling "get()" + var cookies = document.cookie ? document.cookie.split('; ') : []; + var rdecode = /(%[0-9A-Z]{2})+/g; + var i = 0; + + for (; i < cookies.length; i++) { + var parts = cookies[i].split('='); + var cookie = parts.slice(1).join('='); + + if (cookie.charAt(0) === '"') { + cookie = cookie.slice(1, -1); + } + + try { + var name = parts[0].replace(rdecode, decodeURIComponent); + cookie = converter.read ? + converter.read(cookie, name) : converter(cookie, name) || + cookie.replace(rdecode, decodeURIComponent); + + if (this.json) { + try { + cookie = JSON.parse(cookie); + } catch (e) {} + } + + if (key === name) { + result = cookie; + break; + } + + if (!key) { + result[name] = cookie; + } + } catch (e) {} + } + + return result; + } + + api.set = api; + api.get = function (key) { + return api.call(api, key); + }; + api.getJSON = function () { + return api.apply({ + json: true + }, [].slice.call(arguments)); + }; + api.defaults = {}; + + api.remove = function (key, attributes) { + api(key, '', extend(attributes, { + expires: -1 + })); + }; + + api.withConverter = init; + + return api; + } + + return init(function () {}); +}));
\ No newline at end of file |