diff options
author | Lin Jen-Shin <godfat@godfat.org> | 2017-06-06 17:40:08 +0800 |
---|---|---|
committer | Lin Jen-Shin <godfat@godfat.org> | 2017-06-06 17:40:08 +0800 |
commit | d44305eba7fb739447a698e8df123f03dadca9d2 (patch) | |
tree | bda563908cd23f28fcfb67d15be53a0d8d09fb27 /app | |
parent | 47b93fd76138ce24ec78926647497e52c5101dd8 (diff) | |
parent | 4a811fbc11db2d1ab7459f7264848a3b06ff2299 (diff) | |
download | gitlab-ce-d44305eba7fb739447a698e8df123f03dadca9d2.tar.gz |
Merge remote-tracking branch 'upstream/master' into 30634-protected-pipeline
* upstream/master: (56 commits)
Resolve "When changing project visibility setting, change other dropdowns automatically"
change headings to improve SEO
backports changed import logic from pull mirroring feature into CE
Fix header component spec
Add a Rake task to aid in rotating otp_key_base
Remove unnecessary variable
Add changelog entry
Allow users to be hard-deleted from the admin user show page
Allow users to be hard-removed from the admin users list page
Support hard deletion in Admin::UsersController#destroy
Add changelog entry
Extract and memoize `user_access`
Remove GitAccessStatus (no longer needed)
Refactor construction of response
Refactor to remove a special case
Fix would-be regression
Clarify error messages
Refactor to let GitAccess errors bubble up
Refactor to let `GitAccess` check protocol config
Specify new Git-LFS-over-HTTP behavior
...
Diffstat (limited to 'app')
49 files changed, 329 insertions, 203 deletions
diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js index 34e4a257ff9..5b4ca94ed30 100644 --- a/app/assets/javascripts/importer_status.js +++ b/app/assets/javascripts/importer_status.js @@ -56,6 +56,8 @@ if (job.import_status === 'finished') { job_item.removeClass("active").addClass("success"); return status_field.html('<span><i class="fa fa-check"></i> done</span>'); + } else if (job.import_status === 'scheduled') { + return status_field.html("<i class='fa fa-spinner fa-spin'></i> scheduled"); } else if (job.import_status === 'started') { return status_field.html("<i class='fa fa-spinner fa-spin'></i> started"); } else { diff --git a/app/assets/javascripts/locale/zh_CN/app.js b/app/assets/javascripts/locale/zh_CN/app.js new file mode 100644 index 00000000000..9525bc88190 --- /dev/null +++ b/app/assets/javascripts/locale/zh_CN/app.js @@ -0,0 +1 @@ +var locales = locales || {}; locales['zh_CN'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","POT-Creation-Date":"2017-05-04 19:24-0500","PO-Revision-Date":"2017-05-04 19:24-0500","Last-Translator":"HuangTao <htve@outlook.com>, 2017","Language-Team":"Chinese (China) (https://www.transifex.com/gitlab-zh/teams/75177/zh_CN/)","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Language":"zh_CN","Plural-Forms":"nplurals=1; plural=0;","lang":"zh_CN","domain":"app","plural_forms":"nplurals=1; plural=0;"},"ByAuthor|by":["作者:"],"Commit":["提交"],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["周期分析概述了项目从想法到产品实现的各阶段所需的时间。"],"CycleAnalyticsStage|Code":["编码"],"CycleAnalyticsStage|Issue":["议题"],"CycleAnalyticsStage|Plan":["计划"],"CycleAnalyticsStage|Production":["生产"],"CycleAnalyticsStage|Review":["评审"],"CycleAnalyticsStage|Staging":["预发布"],"CycleAnalyticsStage|Test":["测试"],"Deploy":["部署"],"FirstPushedBy|First":["首次推送"],"FirstPushedBy|pushed by":["推送者:"],"From issue creation until deploy to production":["从创建议题到部署至生产环境"],"From merge request merge until deploy to production":["从合并请求被合并后到部署至生产环境"],"Introducing Cycle Analytics":["周期分析简介"],"Last %d day":["最后 %d 天"],"Limited to showing %d event at most":["最多显示 %d 个事件"],"Median":["中位数"],"New Issue":["新议题"],"Not available":["数据不足"],"Not enough data":["数据不足"],"OpenedNDaysAgo|Opened":["开始于"],"Pipeline Health":["流水线健康指标"],"ProjectLifecycle|Stage":["项目生命周期"],"Read more":["了解更多"],"Related Commits":["相关的提交"],"Related Deployed Jobs":["相关的部署作业"],"Related Issues":["相关的议题"],"Related Jobs":["相关的作业"],"Related Merge Requests":["相关的合并请求"],"Related Merged Requests":["相关已合并的合并请求"],"Showing %d event":["显示 %d 个事件"],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["编码阶段概述了从第一次提交到创建合并请求的时间。创建第一个合并请求后,数据将自动添加到此处。"],"The collection of events added to the data gathered for that stage.":["与该阶段相关的事件。"],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["议题阶段概述了从创建议题到将议题设置里程碑或将议题添加到议题看板的时间。开始创建议题以查看此阶段的数据。"],"The phase of the development lifecycle.":["项目生命周期中的各个阶段。"],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["计划阶段概述了从议题添加到日程后到推送首次提交的时间。当首次推送提交后,数据将自动添加到此处。"],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["生产阶段概述了从创建一个议题到将代码部署到生产环境的总时间。当完成想法到部署生产的循环,数据将自动添加到此处。"],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["评审阶段概述了从创建合并请求到被合并的时间。当创建第一个合并请求后,数据将自动添加到此处。"],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["预发布阶段概述了从合并请求被合并到部署至生产环境的总时间。首次部署到生产环境后,数据将自动添加到此处。"],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["测试阶段概述了GitLab CI为相关合并请求运行每个流水线所需的时间。当第一个流水线运行完成后,数据将自动添加到此处。"],"The time taken by each data entry gathered by that stage.":["该阶段每条数据所花的时间"],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["中位数是一个数列中最中间的值。例如在 3、5、9 之间,中位数是 5。在 3、5、7、8 之间,中位数是 (5 + 7)/ 2 = 6。"],"Time before an issue gets scheduled":["议题被列入日程表的时间"],"Time before an issue starts implementation":["开始进行编码前的时间"],"Time between merge request creation and merge/close":["从创建合并请求到被合并或关闭的时间"],"Time until first merge request":["创建第一个合并请求之前的时间"],"Time|hr":["小时"],"Time|min":["分钟"],"Time|s":["秒"],"Total Time":["总时间"],"Total test time for all commits/merges":["所有提交和合并的总测试时间"],"Want to see the data? Please ask an administrator for access.":["权限不足。如需查看相关数据,请向管理员申请权限。"],"We don't have enough data to show this stage.":["该阶段的数据不足,无法显示。"],"You need permission.":["您需要相关的权限。"],"day":["天"]}}};
\ No newline at end of file diff --git a/app/assets/javascripts/locale/zh_HK/app.js b/app/assets/javascripts/locale/zh_HK/app.js new file mode 100644 index 00000000000..fd0bcd988c5 --- /dev/null +++ b/app/assets/javascripts/locale/zh_HK/app.js @@ -0,0 +1 @@ +var locales = locales || {}; locales['zh_HK'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","POT-Creation-Date":"2017-05-04 19:24-0500","PO-Revision-Date":"2017-05-04 19:24-0500","Last-Translator":"HuangTao <htve@outlook.com>, 2017","Language-Team":"Chinese (Hong Kong) (https://www.transifex.com/gitlab-zh/teams/75177/zh_HK/)","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Language":"zh_HK","Plural-Forms":"nplurals=1; plural=0;","lang":"zh_HK","domain":"app","plural_forms":"nplurals=1; plural=0;"},"ByAuthor|by":["作者:"],"Commit":["提交"],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["週期分析概述了項目從想法到產品實現的各階段所需的時間。"],"CycleAnalyticsStage|Code":["編碼"],"CycleAnalyticsStage|Issue":["議題"],"CycleAnalyticsStage|Plan":["計劃"],"CycleAnalyticsStage|Production":["生產"],"CycleAnalyticsStage|Review":["評審"],"CycleAnalyticsStage|Staging":["預發布"],"CycleAnalyticsStage|Test":["測試"],"Deploy":["部署"],"FirstPushedBy|First":["首次推送"],"FirstPushedBy|pushed by":["推送者:"],"From issue creation until deploy to production":["從創建議題到部署到生產環境"],"From merge request merge until deploy to production":["從合併請求的合併到部署至生產環境"],"Introducing Cycle Analytics":["週期分析簡介"],"Last %d day":["最後 %d 天"],"Limited to showing %d event at most":["最多顯示 %d 個事件"],"Median":["中位數"],"New Issue":["新議題"],"Not available":["不可用"],"Not enough data":["數據不足"],"OpenedNDaysAgo|Opened":["開始於"],"Pipeline Health":["流水線健康指標"],"ProjectLifecycle|Stage":["項目生命週期"],"Read more":["了解更多"],"Related Commits":["相關的提交"],"Related Deployed Jobs":["相關的部署作業"],"Related Issues":["相關的議題"],"Related Jobs":["相關的作業"],"Related Merge Requests":["相關的合併請求"],"Related Merged Requests":["相關已合併的合並請求"],"Showing %d event":["顯示 %d 個事件"],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["編碼階段概述了從第一次提交到創建合併請求的時間。創建第壹個合並請求後,數據將自動添加到此處。"],"The collection of events added to the data gathered for that stage.":["與該階段相關的事件。"],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["議題階段概述了從創建議題到將議題設置裏程碑或將議題添加到議題看板的時間。創建一個議題後,數據將自動添加到此處。"],"The phase of the development lifecycle.":["項目生命週期中的各個階段。"],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["計劃階段概述了從議題添加到日程後到推送首次提交的時間。當首次推送提交後,數據將自動添加到此處。"],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["生產階段概述了從創建議題到將代碼部署到生產環境的時間。當完成完整的想法到部署生產,數據將自動添加到此處。"],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["評審階段概述了從創建合並請求到合併的時間。當創建第壹個合並請求後,數據將自動添加到此處。"],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["預發布階段概述了合並請求的合併到部署代碼到生產環境的總時間。當首次部署到生產環境後,數據將自動添加到此處。"],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["測試階段概述了GitLab CI為相關合併請求運行每個流水線所需的時間。當第壹個流水線運行完成後,數據將自動添加到此處。"],"The time taken by each data entry gathered by that stage.":["該階段每條數據所花的時間"],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["中位數是一個數列中最中間的值。例如在 3、5、9 之間,中位數是 5。在 3、5、7、8 之間,中位數是 (5 + 7)/ 2 = 6。"],"Time before an issue gets scheduled":["議題被列入日程表的時間"],"Time before an issue starts implementation":["開始進行編碼前的時間"],"Time between merge request creation and merge/close":["從創建合併請求到被合並或關閉的時間"],"Time until first merge request":["創建第壹個合併請求之前的時間"],"Time|hr":["小時"],"Time|min":["分鐘"],"Time|s":["秒"],"Total Time":["總時間"],"Total test time for all commits/merges":["所有提交和合併的總測試時間"],"Want to see the data? Please ask an administrator for access.":["權限不足。如需查看相關數據,請向管理員申請權限。"],"We don't have enough data to show this stage.":["該階段的數據不足,無法顯示。"],"You need permission.":["您需要相關的權限。"],"day":["天"]}}};
\ No newline at end of file diff --git a/app/assets/javascripts/locale/zh_TW/app.js b/app/assets/javascripts/locale/zh_TW/app.js new file mode 100644 index 00000000000..79904d17bf6 --- /dev/null +++ b/app/assets/javascripts/locale/zh_TW/app.js @@ -0,0 +1 @@ +var locales = locales || {}; locales['zh_TW'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","POT-Creation-Date":"2017-05-04 19:24-0500","PO-Revision-Date":"2017-05-04 19:24-0500","Last-Translator":"HuangTao <htve@outlook.com>, 2017","Language-Team":"Chinese (Taiwan) (https://www.transifex.com/gitlab-zh/teams/75177/zh_TW/)","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Language":"zh_TW","Plural-Forms":"nplurals=1; plural=0;","lang":"zh_TW","domain":"app","plural_forms":"nplurals=1; plural=0;"},"ByAuthor|by":["作者:"],"Commit":["送交"],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["週期分析概述了你的專案從想法到產品實現,各階段所需的時間。"],"CycleAnalyticsStage|Code":["程式開發"],"CycleAnalyticsStage|Issue":["議題"],"CycleAnalyticsStage|Plan":["計劃"],"CycleAnalyticsStage|Production":["上線"],"CycleAnalyticsStage|Review":["複閱"],"CycleAnalyticsStage|Staging":["預備"],"CycleAnalyticsStage|Test":["測試"],"Deploy":["部署"],"FirstPushedBy|First":["首次推送"],"FirstPushedBy|pushed by":["推送者:"],"From issue creation until deploy to production":["從議題建立至線上部署"],"From merge request merge until deploy to production":["從請求被合併後至線上部署"],"Introducing Cycle Analytics":["週期分析簡介"],"Last %d day":["最後 %d 天"],"Limited to showing %d event at most":["最多顯示 %d 個事件"],"Median":["中位數"],"New Issue":["新議題"],"Not available":["無法使用"],"Not enough data":["資料不足"],"OpenedNDaysAgo|Opened":["開始於"],"Pipeline Health":["流水線健康指標"],"ProjectLifecycle|Stage":["專案生命週期"],"Read more":["了解更多"],"Related Commits":["相關的送交"],"Related Deployed Jobs":["相關的部署作業"],"Related Issues":["相關的議題"],"Related Jobs":["相關的作業"],"Related Merge Requests":["相關的合併請求"],"Related Merged Requests":["相關已合併的請求"],"Showing %d event":["顯示 %d 個事件"],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["程式開發階段顯示從第一次送交到建立合併請求的時間。建立第一個合併請求後,資料將自動填入。"],"The collection of events added to the data gathered for that stage.":["與該階段相關的事件。"],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["議題階段顯示從議題建立到設置里程碑、或將該議題加至議題看板的時間。建立第一個議題後,資料將自動填入。"],"The phase of the development lifecycle.":["專案開發生命週期的各個階段。"],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["計劃階段顯示從議題添加到日程後至推送第一個送交的時間。當第一次推送送交後,資料將自動填入。"],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["上線階段顯示從建立一個議題到部署程式至線上的總時間。當完成從想法到產品實現的循環後,資料將自動填入。"],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["複閱階段顯示從合併請求建立後至被合併的時間。當建立第一個合併請求後,資料將自動填入。"],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["預備階段顯示從合併請求被合併後至部署上線的時間。當第一次部署上線後,資料將自動填入。"],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["測試階段顯示相關合併請求的流水線所花的時間。當第一個流水線運作完畢後,資料將自動填入。"],"The time taken by each data entry gathered by that stage.":["每筆該階段相關資料所花的時間。"],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["中位數是一個數列中最中間的值。例如在 3、5、9 之間,中位數是 5。在 3、5、7、8 之間,中位數是 (5 + 7)/ 2 = 6。"],"Time before an issue gets scheduled":["議題被列入日程表的時間"],"Time before an issue starts implementation":["議題等待開始實作的時間"],"Time between merge request creation and merge/close":["合併請求被合併或是關閉的時間"],"Time until first merge request":["第一個合併請求被建立前的時間"],"Time|hr":["小時"],"Time|min":["分鐘"],"Time|s":["秒"],"Total Time":["總時間"],"Total test time for all commits/merges":["所有送交和合併的總測試時間"],"Want to see the data? Please ask an administrator for access.":["權限不足。如需查看相關資料,請向管理員申請權限。"],"We don't have enough data to show this stage.":["因該階段的資料不足而無法顯示相關資訊"],"You need permission.":["您需要相關的權限。"],"day":["天"]}}};
\ No newline at end of file diff --git a/app/assets/javascripts/project_new.js b/app/assets/javascripts/project_new.js index 04b381fe0e0..c0f757269cb 100644 --- a/app/assets/javascripts/project_new.js +++ b/app/assets/javascripts/project_new.js @@ -1,11 +1,17 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-vars, one-var, no-underscore-dangle, prefer-template, no-else-return, prefer-arrow-callback, max-len */ +function highlightChanges($elm) { + $elm.addClass('highlight-changes'); + setTimeout(() => $elm.removeClass('highlight-changes'), 10); +} + (function() { this.ProjectNew = (function() { function ProjectNew() { this.toggleSettings = this.toggleSettings.bind(this); this.$selects = $('.features select'); this.$repoSelects = this.$selects.filter('.js-repo-select'); + this.$projectSelects = this.$selects.not('.js-repo-select'); $('.project-edit-container').on('ajax:before', (function(_this) { return function() { @@ -26,6 +32,42 @@ if (!visibilityContainer) return; const visibilitySelect = new gl.VisibilitySelect(visibilityContainer); visibilitySelect.init(); + + const $visibilitySelect = $(visibilityContainer).find('select'); + let projectVisibility = $visibilitySelect.val(); + const PROJECT_VISIBILITY_PRIVATE = '0'; + + $visibilitySelect.on('change', () => { + const newProjectVisibility = $visibilitySelect.val(); + + if (projectVisibility !== newProjectVisibility) { + this.$projectSelects.each((idx, select) => { + const $select = $(select); + const $options = $select.find('option'); + const values = $.map($options, e => e.value); + + // if switched to "private", limit visibility options + if (newProjectVisibility === PROJECT_VISIBILITY_PRIVATE) { + if ($select.val() !== values[0] && $select.val() !== values[1]) { + $select.val(values[1]).trigger('change'); + highlightChanges($select); + } + $options.slice(2).disable(); + } + + // if switched from "private", increase visibility for non-disabled options + if (projectVisibility === PROJECT_VISIBILITY_PRIVATE) { + $options.enable(); + if ($select.val() !== values[0] && $select.val() !== values[values.length - 1]) { + $select.val(values[values.length - 1]).trigger('change'); + highlightChanges($select); + } + } + }); + + projectVisibility = newProjectVisibility; + } + }); }; ProjectNew.prototype.toggleSettings = function() { @@ -56,8 +98,10 @@ ProjectNew.prototype.toggleRepoVisibility = function () { var $repoAccessLevel = $('.js-repo-access-level select'); + var $lfsEnabledOption = $('.js-lfs-enabled select'); var containerRegistry = document.querySelectorAll('.js-container-registry')[0]; var containerRegistryCheckbox = document.getElementById('project_container_registry_enabled'); + var prevSelectedVal = parseInt($repoAccessLevel.val(), 10); this.$repoSelects.find("option[value='" + $repoAccessLevel.val() + "']") .nextAll() @@ -71,29 +115,40 @@ var $this = $(this); var repoSelectVal = parseInt($this.val(), 10); - $this.find('option').show(); + $this.find('option').enable(); - if (selectedVal < repoSelectVal) { - $this.val(selectedVal); + if (selectedVal < repoSelectVal || repoSelectVal === prevSelectedVal) { + $this.val(selectedVal).trigger('change'); + highlightChanges($this); } - $this.find("option[value='" + selectedVal + "']").nextAll().hide(); + $this.find("option[value='" + selectedVal + "']").nextAll().disable(); }); if (selectedVal) { this.$repoSelects.removeClass('disabled'); + if ($lfsEnabledOption.length) { + $lfsEnabledOption.removeClass('disabled'); + highlightChanges($lfsEnabledOption); + } if (containerRegistry) { containerRegistry.style.display = ''; } } else { this.$repoSelects.addClass('disabled'); + if ($lfsEnabledOption.length) { + $lfsEnabledOption.val('false').addClass('disabled'); + highlightChanges($lfsEnabledOption); + } if (containerRegistry) { containerRegistry.style.display = 'none'; containerRegistryCheckbox.checked = false; } } + + prevSelectedVal = selectedVal; }.bind(this)); }; diff --git a/app/assets/javascripts/protected_tags/protected_tag_dropdown.js b/app/assets/javascripts/protected_tags/protected_tag_dropdown.js index 068e9698e1d..9d045886262 100644 --- a/app/assets/javascripts/protected_tags/protected_tag_dropdown.js +++ b/app/assets/javascripts/protected_tags/protected_tag_dropdown.js @@ -10,7 +10,7 @@ export default class ProtectedTagDropdown { this.$dropdown = options.$dropdown; this.$dropdownContainer = this.$dropdown.parent(); this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer'); - this.$protectedTag = this.$dropdownContainer.find('.create-new-protected-tag'); + this.$protectedTag = this.$dropdownContainer.find('.js-create-new-protected-tag'); this.buildDropdown(); this.bindEvents(); @@ -73,7 +73,7 @@ export default class ProtectedTagDropdown { }; this.$dropdownContainer - .find('.create-new-protected-tag code') + .find('.js-create-new-protected-tag code') .text(tagName); } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index b3a86b92d93..4114a050d9a 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -187,6 +187,7 @@ $divergence-graph-bar-bg: #ccc; $divergence-graph-separator-bg: #ccc; $general-hover-transition-duration: 100ms; $general-hover-transition-curve: linear; +$highlight-changes-color: rgb(235, 255, 232); /* diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 24ab2bedea2..a2f781a6a6e 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -29,6 +29,20 @@ & > .form-group { padding-left: 0; } + + select option[disabled] { + display: none; + } + } + + select { + background: transparent; + transition: background 2s ease-out; + + &.highlight-changes { + background: $highlight-changes-color; + transition: none; + } } .help-block { @@ -675,14 +689,16 @@ pre.light-well { } } -.new_protected_branch { +.new_protected_branch, +.new-protected-tag { label { margin-top: 6px; font-weight: normal; } } -.create-new-protected-branch-button { +.create-new-protected-branch-button, +.create-new-protected-tag-button { @include dropdown-link; width: 100%; diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 563bcc65bd6..bace99dad58 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -138,7 +138,7 @@ class Admin::UsersController < Admin::ApplicationController end def destroy - DeleteUserWorker.perform_async(current_user.id, user.id) + user.delete_async(deleted_by: current_user, params: params.permit(:hard_delete)) respond_to do |format| format.html { redirect_to admin_users_path, notice: "The user is being deleted." } diff --git a/app/controllers/concerns/lfs_request.rb b/app/controllers/concerns/lfs_request.rb index ae91e02488a..2b6afaa6233 100644 --- a/app/controllers/concerns/lfs_request.rb +++ b/app/controllers/concerns/lfs_request.rb @@ -106,4 +106,8 @@ module LfsRequest def objects @objects ||= (params[:objects] || []).to_a end + + def has_authentication_ability?(capability) + (authentication_abilities || []).include?(capability) + end end diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb index 9a1bf037a95..7f3205a8001 100644 --- a/app/controllers/projects/git_http_client_controller.rb +++ b/app/controllers/projects/git_http_client_controller.rb @@ -128,32 +128,10 @@ class Projects::GitHttpClientController < Projects::ApplicationController @authentication_result = Gitlab::Auth.find_for_git_client( login, password, project: project, ip: request.ip) - return false unless @authentication_result.success? - - if download_request? - authentication_has_download_access? - else - authentication_has_upload_access? - end + @authentication_result.success? end def ci? authentication_result.ci?(project) end - - def authentication_has_download_access? - has_authentication_ability?(:download_code) || has_authentication_ability?(:build_download_code) - end - - def authentication_has_upload_access? - has_authentication_ability?(:push_code) - end - - def has_authentication_ability?(capability) - (authentication_abilities || []).include?(capability) - end - - def authentication_project - authentication_result.project - end end diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb index 9e4edcae101..b6b62da7b60 100644 --- a/app/controllers/projects/git_http_controller.rb +++ b/app/controllers/projects/git_http_controller.rb @@ -1,38 +1,27 @@ class Projects::GitHttpController < Projects::GitHttpClientController include WorkhorseRequest + before_action :access_check + + rescue_from Gitlab::GitAccess::UnauthorizedError, with: :render_403 + rescue_from Gitlab::GitAccess::NotFoundError, with: :render_404 + # GET /foo/bar.git/info/refs?service=git-upload-pack (git pull) # GET /foo/bar.git/info/refs?service=git-receive-pack (git push) def info_refs - if upload_pack? && upload_pack_allowed? - log_user_activity - - render_ok - elsif receive_pack? && receive_pack_allowed? - render_ok - elsif http_blocked? - render_http_not_allowed - else - render_denied - end + log_user_activity if upload_pack? + + render_ok end # POST /foo/bar.git/git-upload-pack (git pull) def git_upload_pack - if upload_pack? && upload_pack_allowed? - render_ok - else - render_denied - end + render_ok end # POST /foo/bar.git/git-receive-pack" (git push) def git_receive_pack - if receive_pack? && receive_pack_allowed? - render_ok - else - render_denied - end + render_ok end private @@ -45,10 +34,6 @@ class Projects::GitHttpController < Projects::GitHttpClientController git_command == 'git-upload-pack' end - def receive_pack? - git_command == 'git-receive-pack' - end - def git_command if action_name == 'info_refs' params[:service] @@ -62,47 +47,27 @@ class Projects::GitHttpController < Projects::GitHttpClientController render json: Gitlab::Workhorse.git_http_ok(repository, wiki?, user, action_name) end - def render_http_not_allowed - render plain: access_check.message, status: :forbidden + def render_403(exception) + render plain: exception.message, status: :forbidden end - def render_denied - if user && can?(user, :read_project, project) - render plain: access_denied_message, status: :forbidden - else - # Do not leak information about project existence - render_not_found - end - end - - def access_denied_message - 'Access denied' + def render_404(exception) + render plain: exception.message, status: :not_found end - def upload_pack_allowed? - return false unless Gitlab.config.gitlab_shell.upload_pack - - access_check.allowed? || ci? + def access + @access ||= access_klass.new(access_actor, project, 'http', authentication_abilities: authentication_abilities) end - def access - @access ||= access_klass.new(user, project, 'http', authentication_abilities: authentication_abilities) + def access_actor + return user if user + return :ci if ci? end def access_check # Use the magic string '_any' to indicate we do not know what the # changes are. This is also what gitlab-shell does. - @access_check ||= access.check(git_command, '_any') - end - - def http_blocked? - !access.protocol_allowed? - end - - def receive_pack_allowed? - return false unless Gitlab.config.gitlab_shell.receive_pack - - access_check.allowed? + access.check(git_command, '_any') end def access_klass diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb index a1b84afcd91..4b143434ea5 100644 --- a/app/controllers/projects/imports_controller.rb +++ b/app/controllers/projects/imports_controller.rb @@ -14,14 +14,7 @@ class Projects::ImportsController < Projects::ApplicationController @project.import_url = params[:project][:import_url] if @project.save - @project.reload - - if @project.import_failed? - @project.import_retry - else - @project.import_start - @project.add_import_job - end + @project.reload.import_schedule end redirect_to namespace_project_import_path(@project.namespace, @project) diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb index ba24fa9acfe..d1719f12072 100644 --- a/app/controllers/projects/protected_branches_controller.rb +++ b/app/controllers/projects/protected_branches_controller.rb @@ -19,7 +19,7 @@ class Projects::ProtectedBranchesController < Projects::ProtectedRefsController def protected_ref_params params.require(:protected_branch).permit(:name, - merge_access_levels_attributes: [:access_level, :id], - push_access_levels_attributes: [:access_level, :id]) + merge_access_levels_attributes: access_level_attributes, + push_access_levels_attributes: access_level_attributes) end end diff --git a/app/controllers/projects/protected_refs_controller.rb b/app/controllers/projects/protected_refs_controller.rb index 083a70968e5..b51bdf7aa78 100644 --- a/app/controllers/projects/protected_refs_controller.rb +++ b/app/controllers/projects/protected_refs_controller.rb @@ -44,4 +44,10 @@ class Projects::ProtectedRefsController < Projects::ApplicationController format.js { head :ok } end end + + protected + + def access_level_attributes + %i(access_level id) + end end diff --git a/app/controllers/projects/protected_tags_controller.rb b/app/controllers/projects/protected_tags_controller.rb index c61ddf145e6..a5dbd7e46ae 100644 --- a/app/controllers/projects/protected_tags_controller.rb +++ b/app/controllers/projects/protected_tags_controller.rb @@ -18,6 +18,6 @@ class Projects::ProtectedTagsController < Projects::ProtectedRefsController end def protected_ref_params - params.require(:protected_tag).permit(:name, create_access_levels_attributes: [:access_level, :id]) + params.require(:protected_tag).permit(:name, create_access_levels_attributes: access_level_attributes) end end diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 3ca14dee33c..cd2003586be 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -25,7 +25,7 @@ class RegistrationsController < Devise::RegistrationsController end def destroy - DeleteUserWorker.perform_async(current_user.id, current_user.id) + current_user.delete_async(deleted_by: current_user) respond_to do |format| format.html do diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 7b0584c42a2..f74e61c9481 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -138,11 +138,15 @@ module ProjectsHelper if @project.private? level = @project.project_feature.send(field) - options.delete('Everyone with access') - highest_available_option = options.values.max if level == ProjectFeature::ENABLED + disabled_option = ProjectFeature::ENABLED + highest_available_option = ProjectFeature::PRIVATE if level == disabled_option end - options = options_for_select(options, selected: highest_available_option || @project.project_feature.public_send(field)) + options = options_for_select( + options, + selected: highest_available_option || @project.project_feature.public_send(field), + disabled: disabled_option + ) content_tag( :select, diff --git a/app/helpers/submodule_helper.rb b/app/helpers/submodule_helper.rb index c0763a8a9c4..8e0a1e2ecdf 100644 --- a/app/helpers/submodule_helper.rb +++ b/app/helpers/submodule_helper.rb @@ -13,6 +13,17 @@ module SubmoduleHelper if url =~ /([^\/:]+)\/([^\/]+(?:\.git)?)\Z/ namespace, project = $1, $2 + gitlab_hosts = [Gitlab.config.gitlab.url, + Gitlab.config.gitlab_shell.ssh_path_prefix] + + gitlab_hosts.each do |host| + if url.start_with?(host) + namespace, _, project = url.sub(host, '').rpartition('/') + break + end + end + + namespace.sub!(/\A\//, '') project.rstrip! project.sub!(/\.git\z/, '') diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb index b4aaf498068..50757b01538 100644 --- a/app/helpers/visibility_level_helper.rb +++ b/app/helpers/visibility_level_helper.rb @@ -31,9 +31,9 @@ module VisibilityLevelHelper when Gitlab::VisibilityLevel::PRIVATE "Project access must be granted explicitly to each user." when Gitlab::VisibilityLevel::INTERNAL - "The project can be cloned by any logged in user." + "The project can be accessed by any logged in user." when Gitlab::VisibilityLevel::PUBLIC - "The project can be cloned without any authentication." + "The project can be accessed without any authentication." end end diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb index 0d7c2d20029..4cbd90c5817 100644 --- a/app/models/abuse_report.rb +++ b/app/models/abuse_report.rb @@ -15,8 +15,7 @@ class AbuseReport < ActiveRecord::Base alias_method :author, :reporter def remove_user(deleted_by:) - user.block - DeleteUserWorker.perform_async(deleted_by.id, user.id, delete_solo_owned_groups: true, hard_delete: true) + user.delete_async(deleted_by: deleted_by, params: { hard_delete: true }) end def notify diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 9e04976e8fd..3b49cb4e3bf 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -199,7 +199,7 @@ class ApplicationSetting < ActiveRecord::Base ApplicationSetting.define_attribute_methods end - def self.defaults_ce + def self.defaults { after_sign_up_text: nil, akismet_enabled: false, @@ -250,10 +250,6 @@ class ApplicationSetting < ActiveRecord::Base } end - def self.defaults - defaults_ce - end - def self.create_from_defaults create(defaults) end diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb index 62eaec2407f..47e71c58557 100644 --- a/app/models/concerns/protected_ref.rb +++ b/app/models/concerns/protected_ref.rb @@ -8,32 +8,44 @@ module ProtectedRef validates :project, presence: true delegate :matching, :matches?, :wildcard?, to: :ref_matcher + end + + def commit + project.commit(self.name) + end + + class_methods do + def protected_ref_access_levels(*types) + types.each do |type| + has_many :"#{type}_access_levels", dependent: :destroy + + validates :"#{type}_access_levels", length: { is: 1, message: "are restricted to a single instance per #{self.model_name.human}." } - def self.protected_ref_accessible_to?(ref, user, action:) + accepts_nested_attributes_for :"#{type}_access_levels", allow_destroy: true + end + end + + def protected_ref_accessible_to?(ref, user, action:) access_levels_for_ref(ref, action: action).any? do |access_level| access_level.check_access(user) end end - def self.developers_can?(action, ref) + def developers_can?(action, ref) access_levels_for_ref(ref, action: action).any? do |access_level| access_level.access_level == Gitlab::Access::DEVELOPER end end - def self.access_levels_for_ref(ref, action:) + def access_levels_for_ref(ref, action:) self.matching(ref).map(&:"#{action}_access_levels").flatten end - def self.matching(ref_name, protected_refs: nil) + def matching(ref_name, protected_refs: nil) ProtectedRefMatcher.matching(self, ref_name, protected_refs: protected_refs) end end - def commit - project.commit(self.name) - end - private def ref_matcher diff --git a/app/models/group.rb b/app/models/group.rb index be944da5a67..5bb2cdc5eff 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -222,6 +222,16 @@ class Group < Namespace User.where(id: members_with_parents.select(:user_id)) end + def max_member_access_for_user(user) + return GroupMember::OWNER if user.admin? + + members_with_parents. + where(user_id: user). + reorder(access_level: :desc). + first&. + access_level || GroupMember::NO_ACCESS + end + def mattermost_team_params max_length = 59 diff --git a/app/models/member.rb b/app/models/member.rb index 7228e82e978..29f9d61e870 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -200,6 +200,10 @@ class Member < ActiveRecord::Base source_type end + def access_field + access_level + end + def invite? self.invite_token.present? end diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index 28e10bc6172..47040f95533 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -25,10 +25,6 @@ class GroupMember < Member source end - def access_field - access_level - end - # Because source_type is `Namespace`... def real_source_type 'Group' diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index b3a91feb091..c0e17f4bfc8 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -79,10 +79,6 @@ class ProjectMember < Member end end - def access_field - access_level - end - def project source end diff --git a/app/models/project.rb b/app/models/project.rb index d21ff274b6e..0caf7387450 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -165,7 +165,7 @@ class Project < ActiveRecord::Base has_many :todos, dependent: :destroy has_many :notification_settings, dependent: :destroy, as: :source - has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" + has_one :import_data, dependent: :delete, class_name: "ProjectImportData" has_one :project_feature, dependent: :destroy has_one :statistics, class_name: 'ProjectStatistics', dependent: :delete has_many :container_repositories, dependent: :destroy @@ -298,8 +298,16 @@ class Project < ActiveRecord::Base scope :excluding_project, ->(project) { where.not(id: project) } state_machine :import_status, initial: :none do + event :import_schedule do + transition [:none, :finished, :failed] => :scheduled + end + + event :force_import_start do + transition [:none, :finished, :failed] => :started + end + event :import_start do - transition [:none, :finished] => :started + transition scheduled: :started end event :import_finish do @@ -307,18 +315,23 @@ class Project < ActiveRecord::Base end event :import_fail do - transition started: :failed + transition [:scheduled, :started] => :failed end event :import_retry do transition failed: :started end + state :scheduled state :started state :finished state :failed - after_transition any => :finished, do: :reset_cache_and_import_attrs + after_transition [:none, :finished, :failed] => :scheduled do |project, _| + project.run_after_commit { add_import_job } + end + + after_transition started: :finished, do: :reset_cache_and_import_attrs end class << self @@ -532,9 +545,17 @@ class Project < ActiveRecord::Base end def import_in_progress? + import_started? || import_scheduled? + end + + def import_started? import? && import_status == 'started' end + def import_scheduled? + import_status == 'scheduled' + end + def import_failed? import_status == 'failed' end diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb index 28b7d5ad072..5f0d0802ac9 100644 --- a/app/models/protected_branch.rb +++ b/app/models/protected_branch.rb @@ -2,14 +2,7 @@ class ProtectedBranch < ActiveRecord::Base include Gitlab::ShellAdapter include ProtectedRef - has_many :merge_access_levels, dependent: :destroy - has_many :push_access_levels, dependent: :destroy - - validates :merge_access_levels, length: { is: 1, message: "are restricted to a single instance per protected branch." } - validates :push_access_levels, length: { is: 1, message: "are restricted to a single instance per protected branch." } - - accepts_nested_attributes_for :push_access_levels - accepts_nested_attributes_for :merge_access_levels + protected_ref_access_levels :merge, :push # Check if branch name is marked as protected in the system def self.protected?(project, ref_name) diff --git a/app/models/protected_tag.rb b/app/models/protected_tag.rb index 83964095516..f38109c0e52 100644 --- a/app/models/protected_tag.rb +++ b/app/models/protected_tag.rb @@ -2,11 +2,7 @@ class ProtectedTag < ActiveRecord::Base include Gitlab::ShellAdapter include ProtectedRef - has_many :create_access_levels, dependent: :destroy - - validates :create_access_levels, length: { is: 1, message: "are restricted to a single instance per protected tag." } - - accepts_nested_attributes_for :create_access_levels + protected_ref_access_levels :create def self.protected?(project, ref_name) self.matching(ref_name, protected_refs: project.protected_tags).present? diff --git a/app/models/spam_log.rb b/app/models/spam_log.rb index dd21ee15c6c..56a115d1db4 100644 --- a/app/models/spam_log.rb +++ b/app/models/spam_log.rb @@ -4,8 +4,7 @@ class SpamLog < ActiveRecord::Base validates :user, presence: true def remove_user(deleted_by:) - user.block - DeleteUserWorker.perform_async(deleted_by.id, user.id, delete_solo_owned_groups: true, hard_delete: true) + user.delete_async(deleted_by: deleted_by, params: { hard_delete: true }) end def text diff --git a/app/models/user.rb b/app/models/user.rb index e6eb9d09656..9ed42d6b6f5 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -809,6 +809,11 @@ class User < ActiveRecord::Base system_hook_service.execute_hooks_for(self, :destroy) end + def delete_async(deleted_by:, params: {}) + block if params[:hard_delete] + DeleteUserWorker.perform_async(deleted_by.id, id, params) + end + def notification_service NotificationService.new end diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index 87398303c68..fb07298c6c2 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -4,22 +4,25 @@ class GroupPolicy < BasePolicy return unless @user globally_viewable = @subject.public? || (@subject.internal? && !@user.external?) - member = @subject.users_with_parents.include?(@user) - owner = @user.admin? || @subject.has_owner?(@user) - master = owner || @subject.has_master?(@user) + access_level = @subject.max_member_access_for_user(@user) + owner = access_level >= GroupMember::OWNER + master = access_level >= GroupMember::MASTER + reporter = access_level >= GroupMember::REPORTER can_read = false can_read ||= globally_viewable - can_read ||= member - can_read ||= @user.admin? + can_read ||= access_level >= GroupMember::GUEST can_read ||= GroupProjectsFinder.new(group: @subject, current_user: @user).execute.any? can! :read_group if can_read + if reporter + can! :admin_label + end + # Only group masters and group owners can create new projects if master can! :create_projects can! :admin_milestones - can! :admin_label end # Only group owner and administrators can admin group @@ -31,7 +34,7 @@ class GroupPolicy < BasePolicy can! :create_subgroup if @user.can_create_group end - if globally_viewable && @subject.request_access_enabled && !member + if globally_viewable && @subject.request_access_enabled && access_level == GroupMember::NO_ACCESS can! :request_access end end diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 535d93385e6..e874a2d8789 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -48,15 +48,14 @@ module Projects save_project_and_import_data(import_data) - @project.import_start if @project.import? - after_create_actions if @project.persisted? if @project.errors.empty? - @project.add_import_job if @project.import? + @project.import_schedule if @project.import? else fail(error: @project.errors.full_messages.join(', ')) end + @project rescue ActiveRecord::RecordInvalid => e message = "Unable to save #{e.record.type}: #{e.record.errors.full_messages.join(", ")} " diff --git a/app/services/users/activity_service.rb b/app/services/users/activity_service.rb index facf21a7f5c..ab532a1fdcf 100644 --- a/app/services/users/activity_service.rb +++ b/app/services/users/activity_service.rb @@ -16,7 +16,7 @@ module Users def record_activity Gitlab::UserActivities.record(@author.id) - Rails.logger.debug("Recorded activity: #{@activity} for User ID: #{@author.id} (username: #{@author.username}") + Rails.logger.debug("Recorded activity: #{@activity} for User ID: #{@author.id} (username: #{@author.username})") end end end diff --git a/app/services/users/destroy_service.rb b/app/services/users/destroy_service.rb index 9eb6a600f6b..673afb8b5b9 100644 --- a/app/services/users/destroy_service.rb +++ b/app/services/users/destroy_service.rb @@ -6,12 +6,27 @@ module Users @current_user = current_user end + # Synchronously destroys +user+ + # + # The operation will fail if the user is the sole owner of any groups. To + # force the groups to be destroyed, pass `delete_solo_owned_groups: true` in + # +options+. + # + # The user's contributions will be migrated to a global ghost user. To + # force the contributions to be destroyed, pass `hard_delete: true` in + # +options+. + # + # `hard_delete: true` implies `delete_solo_owned_groups: true`. To perform + # a hard deletion without destroying solo-owned groups, pass + # `delete_solo_owned_groups: false, hard_delete: true` in +options+. def execute(user, options = {}) + delete_solo_owned_groups = options.fetch(:delete_solo_owned_groups, options[:hard_delete]) + unless Ability.allowed?(current_user, :destroy_user, user) raise Gitlab::Access::AccessDeniedError, "#{current_user} tried to destroy user #{user}!" end - if !options[:delete_solo_owned_groups] && user.solo_owned_groups.present? + if !delete_solo_owned_groups && user.solo_owned_groups.present? user.errors[:base] << 'You must transfer ownership or delete groups before you can remove user' return user end diff --git a/app/views/admin/users/_user.html.haml b/app/views/admin/users/_user.html.haml index 46d2e3b3de1..4cf4a57ba18 100644 --- a/app/views/admin/users/_user.html.haml +++ b/app/views/admin/users/_user.html.haml @@ -34,9 +34,15 @@ - if user.access_locked? %li = link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success', data: { confirm: 'Are you sure?' } - - if user.can_be_removed? && can?(current_user, :destroy_user, user) + - if can?(current_user, :destroy_user, user) %li.divider + - if user.can_be_removed? + %li + = link_to 'Remove user', admin_user_path(user), + data: { confirm: "USER #{user.name} WILL BE REMOVED! Are you sure?" }, + method: :delete %li - = link_to 'Delete user', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Consider cancelling this deletion and blocking the user instead. Are you sure?" }, - class: 'btn btn-remove btn-block', - method: :delete + = link_to 'Remove user and contributions', admin_user_path(user, hard_delete: true), + data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and comments authored by this user, and groups owned solely by them, will also be removed! Are you sure?" }, + class: 'btn btn-remove btn-block', + method: :delete diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml index 89d0bbb7126..b556ff056c0 100644 --- a/app/views/admin/users/show.html.haml +++ b/app/views/admin/users/show.html.haml @@ -177,7 +177,7 @@ %p Deleting a user has the following effects: = render 'users/deletion_guidance', user: @user %br - = link_to 'Remove user', [:admin, @user], data: { confirm: "USER #{@user.name} WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-remove" + = link_to 'Remove user', admin_user_path(@user), data: { confirm: "USER #{@user.name} WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-remove" - else - if @user.solo_owned_groups.present? %p @@ -188,3 +188,22 @@ - else %p You don't have access to delete this user. + + .panel.panel-danger + .panel-heading + Remove user and contributions + .panel-body + - if can?(current_user, :destroy_user, @user) + %p + This option deletes the user and any contributions that + would usually be moved to the + = succeed "." do + = link_to "system ghost user", help_page_path("user/profile/account/delete_account") + As well as the user's personal projects, groups owned solely by + the user, and projects in them, will also be removed. Commits + to other projects are unaffected. + %br + = link_to 'Remove user and contributions', admin_user_path(@user, hard_delete: true), data: { confirm: "USER #{@user.name} WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-remove" + - else + %p + You don't have access to delete this user. diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index f5549d7f4cd..c3dab68cea5 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -42,7 +42,7 @@ .col-md-9 .label-light = label_tag :project_visibility, 'Project Visibility', class: 'label-light', for: :project_visibility_level - = link_to "(?)", help_page_path("public_access/public_access") + = link_to icon('question-circle'), help_page_path("public_access/public_access") %span.help-block .col-md-3.visibility-select-container = render('projects/visibility_select', model_method: :visibility_level, form: f, selected_level: @project.visibility_level) @@ -92,14 +92,14 @@ .form-group = render 'shared/allow_request_access', form: f - if Gitlab.config.lfs.enabled && current_user.admin? - .row + .row.js-lfs-enabled .col-md-9 = f.label :lfs_enabled, 'LFS', class: 'label-light' %span.help-block Git Large File Storage = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') .col-md-3 - = f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control', data: { field: 'lfs_enabled' } + = f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control project-repo-select', data: { field: 'lfs_enabled' } - if Gitlab.config.registry.enabled diff --git a/app/views/projects/protected_tags/_create_protected_tag.html.haml b/app/views/projects/protected_tags/_create_protected_tag.html.haml index af9a080f0a2..dd5b346d922 100644 --- a/app/views/projects/protected_tags/_create_protected_tag.html.haml +++ b/app/views/projects/protected_tags/_create_protected_tag.html.haml @@ -1,4 +1,4 @@ -= form_for [@project.namespace.becomes(Namespace), @project, @protected_tag], html: { class: 'js-new-protected-tag' } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @protected_tag], html: { class: 'new-protected-tag js-new-protected-tag' } do |f| .panel.panel-default .panel-heading %h3.panel-title diff --git a/app/views/projects/protected_tags/_dropdown.html.haml b/app/views/projects/protected_tags/_dropdown.html.haml index c8531f96f97..9b6923210f7 100644 --- a/app/views/projects/protected_tags/_dropdown.html.haml +++ b/app/views/projects/protected_tags/_dropdown.html.haml @@ -2,7 +2,7 @@ = dropdown_tag('Select tag or create wildcard', options: { toggle_class: 'js-protected-tag-select js-filter-submit wide git-revision-dropdown-toggle', - filter: true, dropdown_class: "dropdown-menu-selectable capitalize-header git-revision-dropdown", placeholder: "Search protected tag", + filter: true, dropdown_class: "dropdown-menu-selectable capitalize-header git-revision-dropdown", placeholder: "Search protected tags", footer_content: true, data: { show_no: true, show_any: true, show_upcoming: true, selected: params[:protected_tag_name], @@ -10,6 +10,6 @@ %ul.dropdown-footer-list %li - = link_to '#', title: "New Protected Tag", class: "create-new-protected-tag" do + %button{ class: "create-new-protected-tag-button js-create-new-protected-tag", title: "New Protected Tag" } Create wildcard %code diff --git a/app/views/projects/protected_tags/_index.html.haml b/app/views/projects/protected_tags/_index.html.haml index 0bfb1ad191d..663cbd7cd64 100644 --- a/app/views/projects/protected_tags/_index.html.haml +++ b/app/views/projects/protected_tags/_index.html.haml @@ -4,13 +4,14 @@ .row.prepend-top-default.append-bottom-default .col-lg-3 %h4.prepend-top-0 - Protected tags + Protected Tags %p.prepend-top-20 - By default, Protected tags are designed to: + By default, protected tags are designed to: %ul %li Prevent tag creation by everybody except Masters %li Prevent <strong>anyone</strong> from updating the tag %li Prevent <strong>anyone</strong> from deleting the tag + %p.append-bottom-0 Read more about #{link_to "protected tags", help_page_path("user/project/protected_tags"), class: "underlined-link"}. .col-lg-9 - if can? current_user, :admin_project, @project = render 'projects/protected_tags/create_protected_tag' diff --git a/app/views/projects/protected_tags/_protected_tag.html.haml b/app/views/projects/protected_tags/_protected_tag.html.haml index 54249ec0db1..f11ce0483a9 100644 --- a/app/views/projects/protected_tags/_protected_tag.html.haml +++ b/app/views/projects/protected_tags/_protected_tag.html.haml @@ -19,4 +19,4 @@ - if can_admin_project %td - = link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, protected_tag], data: { confirm: 'tag will be writable for developers. Are you sure?' }, method: :delete, class: 'btn btn-warning' + = link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, protected_tag], data: { confirm: 'Tag will be writable for developers. Are you sure?' }, method: :delete, class: 'btn btn-warning' diff --git a/app/views/projects/protected_tags/_tags_list.html.haml b/app/views/projects/protected_tags/_tags_list.html.haml index 728afd75b50..d432a5c9113 100644 --- a/app/views/projects/protected_tags/_tags_list.html.haml +++ b/app/views/projects/protected_tags/_tags_list.html.haml @@ -1,4 +1,4 @@ -.panel.panel-default.protected-tags-list.js-protected-tags-list +.panel.panel-default.protected-tags-list - if @protected_tags.empty? .panel-heading %h3.panel-title @@ -13,6 +13,8 @@ %col{ width: "25%" } %col{ width: "25%" } %col{ width: "50%" } + - if can_admin_project + %col %thead %tr %th Protected tag (#{@protected_tags.size}) diff --git a/app/views/projects/protected_tags/show.html.haml b/app/views/projects/protected_tags/show.html.haml index 94c3612a449..16fc02fe9f4 100644 --- a/app/views/projects/protected_tags/show.html.haml +++ b/app/views/projects/protected_tags/show.html.haml @@ -5,7 +5,7 @@ %h4.prepend-top-0.ref-name = @protected_ref.name - .col-lg-9 + .col-lg-9.edit_protected_tag %h5 Matching Tags - if @matching_refs.present? .table-responsive diff --git a/app/views/sent_notifications/unsubscribe.html.haml b/app/views/sent_notifications/unsubscribe.html.haml index 9ce6a1aeef5..de52fd00157 100644 --- a/app/views/sent_notifications/unsubscribe.html.haml +++ b/app/views/sent_notifications/unsubscribe.html.haml @@ -1,16 +1,14 @@ - noteable = @sent_notification.noteable -- noteable_type = @sent_notification.noteable_type.humanize(capitalize: false) +- noteable_type = @sent_notification.noteable_type.titleize.downcase - noteable_text = %(#{noteable.title} (#{noteable.to_reference})) - -- page_title "Unsubscribe", noteable_text, @sent_notification.noteable_type.humanize.pluralize, @sent_notification.project.name_with_namespace - +- page_title "Unsubscribe", noteable_text, noteable_type.pluralize, @sent_notification.project.name_with_namespace %h3.page-title - Unsubscribe from #{noteable_type} #{noteable_text} + Unsubscribe from #{noteable_type} %p = succeed '?' do - Are you sure you want to unsubscribe from #{noteable_type} + Are you sure you want to unsubscribe from the #{noteable_type}: = link_to noteable_text, url_for([@sent_notification.project.namespace.becomes(Namespace), @sent_notification.project, noteable]) %p diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index cf0540afb38..fbc335f6176 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -7,7 +7,7 @@ - show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && project.commit - css_class += " no-description" if project.description.blank? && !show_last_commit_as_description - cache_key = project_list_cache_key(project) -- updated_tooltip = time_ago_with_tooltip(project.updated_at) +- updated_tooltip = time_ago_with_tooltip(project.last_activity_at) %li.project-row{ class: css_class } = cache(cache_key) do diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb index efc99ec962a..a338523dc6b 100644 --- a/app/workers/repository_fork_worker.rb +++ b/app/workers/repository_fork_worker.rb @@ -1,4 +1,6 @@ class RepositoryForkWorker + ForkError = Class.new(StandardError) + include Sidekiq::Worker include Gitlab::ShellAdapter include DedicatedSidekiqQueue @@ -8,29 +10,31 @@ class RepositoryForkWorker source_path: source_path, target_path: target_path) - project = Project.find_by_id(project_id) - - unless project.present? - logger.error("Project #{project_id} no longer exists!") - return - end + project = Project.find(project_id) + project.import_start result = gitlab_shell.fork_repository(forked_from_repository_storage_path, source_path, project.repository_storage_path, target_path) - unless result - logger.error("Unable to fork project #{project_id} for repository #{source_path} -> #{target_path}") - project.mark_import_as_failed('The project could not be forked.') - return - end + raise ForkError, "Unable to fork project #{project_id} for repository #{source_path} -> #{target_path}" unless result project.repository.after_import - - unless project.valid_repo? - logger.error("Project #{project_id} had an invalid repository after fork") - project.mark_import_as_failed('The forked repository is invalid.') - return - end + raise ForkError, "Project #{project_id} had an invalid repository after fork" unless project.valid_repo? project.import_finish + rescue ForkError => ex + fail_fork(project, ex.message) + raise + rescue => ex + return unless project + + fail_fork(project, ex.message) + raise ForkError, "#{ex.class} #{ex.message}" + end + + private + + def fail_fork(project, message) + Rails.logger.error(message) + project.mark_import_as_failed(message) end end diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb index b33ba2ed7c1..625476b7e01 100644 --- a/app/workers/repository_import_worker.rb +++ b/app/workers/repository_import_worker.rb @@ -1,4 +1,6 @@ class RepositoryImportWorker + ImportError = Class.new(StandardError) + include Sidekiq::Worker include DedicatedSidekiqQueue @@ -10,6 +12,8 @@ class RepositoryImportWorker @project = Project.find(project_id) @current_user = @project.creator + project.import_start + Gitlab::Metrics.add_event(:import_repository, import_url: @project.import_url, path: @project.path_with_namespace) @@ -17,13 +21,23 @@ class RepositoryImportWorker project.update_columns(import_jid: self.jid, import_error: nil) result = Projects::ImportService.new(project, current_user).execute - - if result[:status] == :error - project.mark_import_as_failed(result[:message]) - return - end + raise ImportError, result[:message] if result[:status] == :error project.repository.after_import project.import_finish + rescue ImportError => ex + fail_import(project, ex.message) + raise + rescue => ex + return unless project + + fail_import(project, ex.message) + raise ImportError, "#{ex.class} #{ex.message}" + end + + private + + def fail_import(project, message) + project.mark_import_as_failed(message) end end |