Commit 2c9efd82 authored by Phil Hughes's avatar Phil Hughes

Merge branch 'master' into ide-workbench-bar

parents 86fa0469 87e592dc
......@@ -2,6 +2,213 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 10.7.0 (2018-04-22)
### Security (6 changes, 2 of them are from the community)
- Fixed some SSRF vulnerabilities in services, hooks and integrations. !2337
- Update ruby-saml to 1.7.2 and omniauth-saml to 1.10.0. !17734 (Takuya Noguchi)
- Update rack-protection to 2.0.1. !17835 (Takuya Noguchi)
- Adds confidential notes channel for Slack/Mattermost.
- Fix XSS on diff view stored on filenames.
- Fix GitLab Auth0 integration signing in the wrong user.
### Fixed (65 changes, 20 of them are from the community)
- File uploads in remote storage now support project renaming. !4597
- Fixed bug in dropdown selector when selecting the same selection again. !14631 (bitsapien)
- Fixed group deletion linked to Mattermost. !16209 (Julien Millau)
- Create commit API and Web IDE obey LFS filters. !16718
- Set breadcrumb for admin/runners/show. !17431 (Takuya Noguchi)
- Enable restore rake task to handle nested storage directories. !17516 (Balasankar C)
- Fix hover style of dropdown items in the right sidebar. !17519
- Improve empty state for canceled job. !17646
- Fix generated URL when listing repoitories for import. !17692
- Use singular in the diff stats if only one line has been changed. !17697 (Jan Beckmann)
- Long instance urls do not overflow anymore during project creation. !17717
- Fix importing multiple assignees from GitLab export. !17718
- Correct copy text for the promote milestone and label modals. !17726
- Fix search results stripping last endline when parsing the results. !17777 (Jasper Maes)
- Add read-only banner to all pages. !17798
- Fix viewing diffs on old merge requests. !17805
- Fix forking to subgroup via API when namespace is given by name. !17815 (Jan Beckmann)
- Fix UI breakdown for Create merge request button. !17821 (Takuya Noguchi)
- Unify format for nested non-task lists. !17823 (Takuya Noguchi)
- UX re-design branch items with flexbox. !17832 (Takuya Noguchi)
- Use porcelain commit lookup method on CI::CreatePipelineService. !17911
- Update dashboard milestones breadcrumb link. !17933 (George Tsiolis)
- Deleting a MR you are assigned to should decrements counter. !17951 (m b)
- Update no repository placeholder. !17964 (George Tsiolis)
- Drop JSON response in Project Milestone along with avoiding error. !17977 (Takuya Noguchi)
- Fix personal access token clipboard button style. !17978 (Fabian Schneider)
- Avoid validation errors when running the Pages domain verification service. !17992
- Project creation will now raise an error if a service template is invalid. !18013
- Add better LDAP connection handling. !18039
- Fix autolinking URLs containing ampersands. !18045
- Fix exceptions raised when migrating pipeline stages in the background. !18076
- Always display Labels section in issuable sidebar, even when the project has no labels. !18081 (Branka Martinovic)
- Fixed gitlab:uploads:migrate task ignoring some uploads. !18082
- Fixed gitlab:uploads:migrate task failing for Groups' avatar. !18088
- Increase dropdown width in pipeline graph & center action icon. !18089
- Fix `JobsController#raw` endpoint can not read traces in database. !18101
- Fix `gitlab-rake gitlab:two_factor:disable_for_all_users`. !18154
- Adjust 404's for LegacyDiffNote discussion rendering. !18201
- Work around Prometheus Helm chart name changes to fix integration. !18206 (joshlambert)
- Prioritize weight over title when sorting charts. !18233
- Verify that deploy token has valid access when pulling container registry image. !18260
- Stop redirecting the page in pipeline main actions.
- Fixed IDE button opening the wrong URL in tree list.
- Ensure hooks run when a deploy key without a user pushes.
- Fix 404 in group boards when moving issue between lists.
- Display state indicator for issuable references in non-project scope (e.g. when referencing issuables from group scope).
- Add missing port to artifact links.
- Fix data race between ObjectStorage background_upload and Pages publishing.
- Fixes unresolved discussions rendering the error state instead of the diff.
- Don't show Jump to Discussion button on Issues.
- Fix bug rendering group icons when forking.
- Automatically cleanup stale worktrees and lock files upon a push.
- Use the GitLab version as part of the appearances cache key.
- Fix Firefox stealing formatting characters on issue notes.
- Include matching branches and tags in protected branches / tags count. (Jan Beckmann)
- Fix 500 error when a merge request from a fork has conflicts and has not yet been updated.
- Test if remote repository exists when importing wikis.
- Hide emoji popup after multiple spaces. (Jan Beckmann)
- Fix relative uri when "#" is in branch name. (Jan)
- Escape Markdown characters properly when using autocomplete.
- Ignore project internal references in group context.
- Fix finding wiki file when Gitaly is enabled.
- Fix listing commit branch/tags that contain special characters.
- Ensure internal users (ghost, support bot) get assigned a namespace.
- Fix links to subdirectories of a directory with a plus character in its path.
### Deprecated (1 change)
- Remove support for legacy tar.gz pages artifacts. !18090
### Changed (22 changes, 2 of them are from the community)
- Add yellow favicon when `CANARY=true` to differientate canary environment. !12477
- Use human readable value build_timeout in Project. !17386
- Improved visual styles and consistency for commit hash and possible actions across commit lists. !17406
- Don't create permanent redirect routes. !17521
- Add empty repo check before running AutoDevOps pipeline. !17605
- Update wording to specify create/manage project vs group labels in labels dropdown. !17640
- Add tooltips to icons in lists of issues and merge requests. !17700
- Change avatar error message to include allowed file formats. !17747 (Fabian Schneider)
- Polish design for verifying domains. !17767
- Move email footer info to a single line. !17916
- Add average and maximum summary statistics to the prometheus dashboard. !17921
- Add additional cluster usage metrics to usage ping. !17922
- Move 'Registry' after 'CI/CD' in project navigation sidebar. !18018 (Elias Werberich)
- Redesign application settings to match project settings. !18019
- Allow HTTP(s) when git request is made by GitLab CI. !18021
- Added hover background color to IDE file list rows.
- Make project avatar in IDE consistent with the rest of GitLab.
- Show issues of subgroups in group-level issue board.
- Repository checksum calculation is handled by Gitaly when feature is enabled.
- Allow viewing timings for AJAX requests in the performance bar.
- Fixes remove source branch checkbox being visible when user cannot remove the branch.
- Make /-/ delimiter optional for search endpoints.
### Performance (24 changes, 11 of them are from the community)
- Move AssigneeTitle vue component. !17397 (George Tsiolis)
- Move TimeTrackingCollapsedState vue component. !17399 (George Tsiolis)
- Move MemoryGraph and MemoryUsage vue components. !17533 (George Tsiolis)
- Move UnresolvedDiscussions vue component. !17538 (George Tsiolis)
- Move NothingToMerge vue component. !17544 (George Tsiolis)
- Move ShaMismatch vue component. !17546 (George Tsiolis)
- Stop caching highlighted diffs in Redis unnecessarily. !17746
- Add i18n and update specs for ShaMismatch vue component. !17870 (George Tsiolis)
- Update spec import path for vue mount component helper. !17880 (George Tsiolis)
- Move TimeTrackingComparisonPane vue component. !17931 (George Tsiolis)
- Improves the performance of projects list page. !17934
- Remove N+1 query for Noteable association. !17956
- Improve performance of loading issues with lots of references to merge requests. !17986
- Reuse root_ref_hash for performance on Branches. !17998 (Takuya Noguchi)
- Update asciidoctor-plantuml to 0.0.8. !18022 (Takuya Noguchi)
- Cache personal projects count. !18197
- Reduce complexity of issuable finder query. !18219
- Reduce number of queries when viewing a merge request.
- Free open file descriptors and libgit2 buffers in UpdatePagesService.
- Memoize Git::Repository#has_visible_content?.
- Require at least one filter when listing issues or merge requests on dashboard page.
- lazy load diffs on merge request discussions.
- Bulk deleting refs is handled by Gitaly by default.
- ListCommitsByOid is executed by Gitaly by default.
### Added (38 changes, 7 of them are from the community)
- Add HTTPS-only pages. !16273 (rfwatson)
- adds closed by informations in issue api. !17042 (haseebeqx)
- Projects and groups badges settings UI. !17114
- Add per-runner configured job timeout. !17221
- Add alternate archive route for simplified packaging. !17225
- Add support for pipeline variables expressions in only/except. !17316
- Add object storage support for LFS objects, CI artifacts, and uploads. !17358
- Added confirmation modal for changing username. !17405
- Implement foreground verification of CI artifacts. !17578
- Extend API for exporting a project with direct upload URL. !17686
- Move ci/lint under project's namespace. !17729
- Add Total CPU/Memory consumption metrics for Kubernetes. !17731
- Adds the option to the project export API to override the project description and display GitLab export description once imported. !17744
- Port direct upload of LFS artifacts from EE. !17752
- Adds support for OmniAuth JWT provider. !17774
- Display error message on job's tooltip if this one fails. !17782
- Add 'Assigned Issues' and 'Assigned Merge Requests' as dashboard view choices for users. !17860 (Elias Werberich)
- Extend API for importing a project export with overwrite support. !17883
- Create Deploy Tokens to allow permanent access to repository and registry. !17894
- Detect commit message trailers and link users properly to their accounts on Gitlab. !17919 (cousine)
- Adds cancel btn to new pages domain page. !18026 (Jacopo Beschi @jacopo-beschi)
- API: Add parameter merge_method to projects. !18031 (Jan Beckmann)
- Introduce simpler env vars for auto devops REPLICAS and CANARY_REPLICAS #41436. !18036
- Allow overriding params on project import through API. !18086
- Support LFS objects when importing/exporting GitLab project archives. !18115
- Store sha256 checksum of artifact metadata. !18149
- Limit the number of failed logins when using LDAP for authentication. !43525
- Allow assigning and filtering issuables by ancestor group labels.
- Include subgroup issues when searching for group issues using the API.
- Allow to store uploads by default on Object Storage.
- Add slash command for moving issues. (Adam Pahlevi)
- Render MR commit SHA instead "diffs" when viable.
- Send @mention notifications even if a user has explicitly unsubscribed from item.
- Add support for Sidekiq JSON logging.
- Add Gitaly call details to performance bar.
- Add support for patch link extension for commit links on GitLab Flavored Markdown.
- Allow feature gates to be removed through the API.
- Allow merge requests related to a commit to be found via API.
### Other (27 changes, 11 of them are from the community)
- Send notification emails when push to a merge request. !7610 (YarNayar)
- Rename modal.vue to deprecated_modal.vue. !17438
- Atomic generation of internal ids for issues. !17580
- Use object ID to prevent duplicate keys Vue warning on Issue Boards page during development. !17682
- Update foreman from 0.78.0 to 0.84.0. !17690 (Takuya Noguchi)
- Add realtime pipeline status for adding/viewing files. !17705
- Update documentation to reflect current minimum required versions of node and yarn. !17706
- Update knapsack to 1.16.0. !17735 (Takuya Noguchi)
- Update CI services documnetation. !17749
- Added i18n support for the prometheus memory widget. !17753
- Use specific names for filtered CI variable controller parameters. !17796
- Apply NestingDepth (level 5) (framework/dropdowns.scss). !17820 (Takuya Noguchi)
- Clean up selectors in framework/header.scss. !17822 (Takuya Noguchi)
- Bump `state_machines-activerecord` to 0.5.1. !17924 (blackst0ne)
- Increase the memory limits used in the unicorn killer. !17948
- Replace the spinach test with an rspec analog. !17950 (blackst0ne)
- Remove unused index from events table. !18014
- Make all workhorse gitaly calls opt-out, take 2. !18043
- Update brakeman 3.6.1 to 4.2.1. !18122 (Takuya Noguchi)
- Replace the `project/issues/labels.feature` spinach test with an rspec analog. !18126 (blackst0ne)
- Bump html-pipeline to 2.7.1. !18132 (@blackst0ne)
- Remove test_ci rake task. !18139 (Takuya Noguchi)
- Add documentation for Pipelines failure reasons. !18352
- Improve JIRA event descriptions.
- Add query counts to profiler output.
- Move Sidekiq exporter logs to log/sidekiq_exporter.log.
- Upgrade Gitaly to upgrade its charlock_holmes.
## 10.6.4 (2018-04-09)
### Fixed (8 changes, 1 of them is from the community)
......
......@@ -126,7 +126,7 @@ Most issues will have labels for at least one of the following:
- Type: ~"feature proposal", ~bug, ~customer, etc.
- Subject: ~wiki, ~"container registry", ~ldap, ~api, ~frontend, etc.
- Team: ~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.
- Priority: ~Deliverable, ~Stretch, ~"Next Patch Release"
- Milestone: ~Deliverable, ~Stretch, ~"Next Patch Release"
All labels, their meaning and priority are defined on the
[labels page][labels-page].
......@@ -185,10 +185,10 @@ indicate if an issue needs backend work, frontend work, or both.
Team labels are always capitalized so that they show up as the first label for
any issue.
### Priority labels (~Deliverable, ~Stretch, ~"Next Patch Release")
### Milestone labels (~Deliverable, ~Stretch, ~"Next Patch Release")
Priority labels help us clearly communicate expectations of the work for the
release. There are two levels of priority labels:
Milestone labels help us clearly communicate expectations of the work for the
release. There are three levels of Milestone labels:
- ~Deliverable: Issues that are expected to be delivered in the current
milestone.
......@@ -203,16 +203,46 @@ Each issue scheduled for the current milestone should be labeled ~Deliverable
or ~"Stretch". Any open issue for a previous milestone should be labeled
~"Next Patch Release", or otherwise rescheduled to a different milestone.
### Severity labels (~S1, ~S2, etc.)
### Bug Priority labels (~P1, ~P2, ~P3 & etc.)
Bug Priority labels help us define the time a ~bug fix should be completed. Priority determines how quickly the defect turnaround time must be. If there are multiple defects, the priority decides which defect has to be fixed immediately versus later.
This label documents the planned timeline & urgency which is used to measure against our actual SLA on delivering ~bug fixes.
| Label | Meaning | Estimate time to fix | Guidance |
|-------|-----------------|------------------------------------------------------------------|----------|
| ~P1 | Urgent Priority | The current release | |
| ~P2 | High Priority | The next release | |
| ~P3 | Medium Priority | Within the next 3 releases (approx one quarter) | |
| ~P4 | Low Priority | Anything outside the next 3 releases (approx beyond one quarter) | The issue is prominent but does not impact user workflow and a workaround is documented |
#### Specific Priority guidance
| Label | Availability / Performance |
|-------|--------------------------------------------------------------|
| ~P1 | |
| ~P2 | The issue is (almost) guaranteed to occur in the near future |
| ~P3 | The issue is likely to occur in the near future |
| ~P4 | The issue _may_ occur but it's not likely |
### Bug Severity labels (~S1, ~S2, ~S3 & etc.)
Severity labels help us clearly communicate the impact of a ~bug on users.
| Label | Meaning | Example |
|-------|------------------------------------------|---------|
| ~S1 | Feature broken, no workaround | Unable to create an issue |
| ~S2 | Feature broken, workaround unacceptable | Can push commits, but only via the command line |
| ~S3 | Feature broken, workaround acceptable | Can create merge requests only from the Merge Requests page, not through the Issue |
| ~S4 | Cosmetic issue | Label colors are incorrect / not being displayed |
| Label | Meaning | Impact of the defect | Example |
|-------|-------------------|-------------------------------------------------------|---------|
| ~S1 | Blocker | Outage, broken feature with no workaround | Unable to create an issue. Data corruption/loss. Security breach. |
| ~S2 | Critical Severity | Broken Feature, workaround too complex & unacceptable | Can push commits, but only via the command line. |
| ~S3 | Major Severity | Broken Feature, workaround acceptable | Can create merge requests only from the Merge Requests page, not through the Issue. |
| ~S4 | Low Severity | Functionality inconvenience or cosmetic issue | Label colors are incorrect / not being displayed. |
#### Specific Severity guidance
| Label | Security Impact |
|-------|-------------------------------------------------------------------|
| ~S1 | >50% customers impacted (possible company extinction level event) |
| ~S2 | Multiple customers impacted (but not apocalyptic) |
| ~S3 | A single customer impacted |
| ~S4 | No customer impact, or expected impact within 30 days |
### Label for community contributors (~"Accepting Merge Requests")
......
Copyright (c) 2011-2017 GitLab B.V.
Copyright GitLab B.V.
With regard to the GitLab Software:
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
For all third party components incorporated into the GitLab Software, those
components are licensed under the original license provided by the owner of the
applicable component.
\ No newline at end of file
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
......@@ -67,6 +67,12 @@ You can access a new installation with the login **`root`** and password **`5ive
GitLab is an open source project and we are very happy to accept community contributions. Please refer to [CONTRIBUTING.md](/CONTRIBUTING.md) for details.
## Licensing
GitLab Community Edition (CE) is available freely under the MIT Expat license.
All third party components incorporated into the GitLab Software are licensed under the original license provided by the owner of the applicable component.
## Install a development environment
To work on GitLab itself, we recommend setting up your development environment with [the GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit).
......
......@@ -113,6 +113,8 @@ class List {
issue.id = data.id;
issue.iid = data.iid;
issue.project = data.project;
issue.path = data.real_path;
issue.referencePath = data.reference_path;
if (this.issuesSize > 1) {
const moveBeforeId = this.issues[1].id;
......
......@@ -84,20 +84,21 @@ export default class CreateMergeRequestDropdown {
if (data.can_create_branch) {
this.available();
this.enable();
this.updateBranchName(data.suggested_branch_name);
if (!this.droplabInitialized) {
this.droplabInitialized = true;
this.initDroplab();
this.bindEvents();
}
} else if (data.has_related_branch) {
} else {
this.hide();
}
})
.catch(() => {
this.unavailable();
this.disable();
Flash('Failed to check if a new branch can be created.');
Flash(__('Failed to check related branches.'));
});
}
......@@ -409,13 +410,16 @@ export default class CreateMergeRequestDropdown {
this.unavailableButton.classList.remove('hide');
}
updateBranchName(suggestedBranchName) {
this.branchInput.value = suggestedBranchName;
this.updateCreatePaths('branch', suggestedBranchName);
}
updateInputState(target, ref, result) {
// target - 'branch' or 'ref' - which the input field we are searching a ref for.
// ref - string - what a user typed.
// result - string - what has been found on backend.
const pathReplacement = `$1${ref}`;
// If a found branch equals exact the same text a user typed,
// that means a new branch cannot be created as it already exists.
if (ref === result) {
......@@ -426,18 +430,12 @@ export default class CreateMergeRequestDropdown {
this.refIsValid = true;
this.refInput.dataset.value = ref;
this.showAvailableMessage('ref');
this.createBranchPath = this.createBranchPath.replace(this.regexps.ref.createBranchPath,
pathReplacement);
this.createMrPath = this.createMrPath.replace(this.regexps.ref.createMrPath,
pathReplacement);
this.updateCreatePaths(target, ref);
}
} else if (target === 'branch') {
this.branchIsValid = true;
this.showAvailableMessage('branch');
this.createBranchPath = this.createBranchPath.replace(this.regexps.branch.createBranchPath,
pathReplacement);
this.createMrPath = this.createMrPath.replace(this.regexps.branch.createMrPath,
pathReplacement);
this.updateCreatePaths(target, ref);
} else {
this.refIsValid = false;
this.refInput.dataset.value = ref;
......@@ -457,4 +455,15 @@ export default class CreateMergeRequestDropdown {
this.disableCreateAction();
}
}
// target - 'branch' or 'ref'
// ref - string - the new value to use as branch or ref
updateCreatePaths(target, ref) {
const pathReplacement = `$1${ref}`;
this.createBranchPath = this.createBranchPath.replace(this.regexps[target].createBranchPath,
pathReplacement);
this.createMrPath = this.createMrPath.replace(this.regexps[target].createMrPath,
pathReplacement);
}
}
......@@ -2,7 +2,9 @@
import $ from 'jquery';
import Pikaday from 'pikaday';
import { __ } from '~/locale';
import axios from './lib/utils/axios_utils';
import { timeFor } from './lib/utils/datetime_utility';
import { parsePikadayDate, pikadayToString } from './lib/utils/datefix';
class DueDateSelect {
......@@ -14,6 +16,7 @@ class DueDateSelect {
this.$dropdownParent = $dropdownParent;
this.$datePicker = $dropdownParent.find('.js-due-date-calendar');
this.$block = $block;
this.$sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon');
this.$selectbox = $dropdown.closest('.selectbox');
this.$value = $block.find('.value');
this.$valueContent = $block.find('.value-content');
......@@ -128,7 +131,8 @@ class DueDateSelect {
submitSelectedDate(isDropdown) {
const selectedDateValue = this.datePayload[this.abilityName].due_date;
const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value';
const hasDueDate = this.displayedDate !== 'No due date';
const displayedDateStyle = hasDueDate ? 'bold' : 'no-value';
this.$loading.removeClass('hidden').fadeIn();
......@@ -145,10 +149,13 @@ class DueDateSelect {
return axios.put(this.issueUpdateURL, this.datePayload)
.then(() => {
const tooltipText = hasDueDate ? `${__('Due date')}<br />${selectedDateValue} (${timeFor(selectedDateValue)})` : __('Due date');
if (isDropdown) {
this.$dropdown.trigger('loaded.gl.dropdown');
this.$dropdown.dropdown('toggle');
}
this.$sidebarCollapsedValue.attr('data-original-title', tooltipText);
return this.$loading.fadeOut();
});
}
......
......@@ -32,7 +32,7 @@ export default class Model {
);
}
this.events = new Map();
this.events = new Set();
this.updateContent = this.updateContent.bind(this);
this.updateNewContent = this.updateNewContent.bind(this);
......@@ -76,10 +76,11 @@ export default class Model {
}
onChange(cb) {
this.events.set(
this.path,
this.disposable.add(this.model.onDidChangeContent(e => cb(this, e))),
);
this.events.add(this.disposable.add(this.model.onDidChangeContent(e => cb(this, e))));
}
onDispose(cb) {
this.events.add(cb);
}
updateContent({ content, changed }) {
......@@ -96,6 +97,11 @@ export default class Model {
dispose() {
this.disposable.dispose();
this.events.forEach(cb => {
if (typeof cb === 'function') cb();
});
this.events.clear();
eventHub.$off(`editor.update.model.dispose.${this.file.key}`, this.dispose);
......
......@@ -38,6 +38,15 @@ export default class DecorationsController {
);
}
hasDecorations(model) {
return this.decorations.has(model.url);
}
removeDecorations(model) {
this.decorations.delete(model.url);
this.editorDecorations.delete(model.url);
}
dispose() {
this.decorations.clear();
this.editorDecorations.clear();
......
......@@ -3,7 +3,7 @@ import { throttle } from 'underscore';
import DirtyDiffWorker from './diff_worker';
import Disposable from '../common/disposable';
export const getDiffChangeType = (change) => {
export const getDiffChangeType = change => {
if (change.modified) {
return 'modified';
} else if (change.added) {
......@@ -16,12 +16,7 @@ export const getDiffChangeType = (change) => {
};
export const getDecorator = change => ({
range: new monaco.Range(
change.lineNumber,
1,
change.endLineNumber,
1,
),
range: new monaco.Range(change.lineNumber, 1, change.endLineNumber, 1),
options: {
isWholeLine: true,
linesDecorationsClassName: `dirty-diff dirty-diff-${getDiffChangeType(change)}`,
......@@ -31,6 +26,7 @@ export const getDecorator = change => ({
export default class DirtyDiffController {
constructor(modelManager, decorationsController) {
this.disposable = new Disposable();
this.models = new Map();
this.editorSimpleWorker = null;
this.modelManager = modelManager;
this.decorationsController = decorationsController;
......@@ -42,7 +38,15 @@ export default class DirtyDiffController {
}
attachModel(model) {
if (this.models.has(model.url)) return;
model.onChange(() => this.throttledComputeDiff(model));
model.onDispose(() => {
this.decorationsController.removeDecorations(model);
this.models.delete(model.url);
});
this.models.set(model.url, model);
}
computeDiff(model) {
......@@ -54,7 +58,11 @@ export default class DirtyDiffController {
}
reDecorate(model) {
if (this.decorationsController.hasDecorations(model)) {
this.decorationsController.decorate(model);
} else {
this.computeDiff(model);
}
}
decorate({ data }) {
......@@ -65,6 +73,7 @@ export default class DirtyDiffController {
dispose() {
this.disposable.dispose();
this.models.clear();
this.dirtyDiffWorker.removeEventListener('message', this.decorate);
this.dirtyDiffWorker.terminate();
......
......@@ -83,7 +83,7 @@ export default class LabelsSelect {
$dropdown.trigger('loading.gl.dropdown');
axios.put(issueUpdateURL, data)
.then(({ data }) => {
var labelCount, template, labelTooltipTitle, labelTitles;
var labelCount, template, labelTooltipTitle, labelTitles, formattedLabels;
$loading.fadeOut();
$dropdown.trigger('loaded.gl.dropdown');
$selectbox.hide();
......@@ -115,8 +115,7 @@ export default class LabelsSelect {
labelTooltipTitle = labelTitles.join(', ');
}
else {
labelTooltipTitle = '';
$sidebarLabelTooltip.tooltip('destroy');
labelTooltipTitle = __('Labels');
}
$sidebarLabelTooltip
......
......@@ -4,6 +4,7 @@
import $ from 'jquery';
import _ from 'underscore';
import { __ } from '~/locale';
import axios from './lib/utils/axios_utils';
import { timeFor } from './lib/utils/datetime_utility';
import ModalStore from './boards/stores/modal_store';
......@@ -25,7 +26,7 @@ export default class MilestoneSelect {
}
$els.each((i, dropdown) => {
let collapsedSidebarLabelTemplate, milestoneLinkNoneTemplate, milestoneLinkTemplate, selectedMilestone, selectedMilestoneDefault;
let milestoneLinkNoneTemplate, milestoneLinkTemplate, selectedMilestone, selectedMilestoneDefault;
const $dropdown = $(dropdown);
const projectId = $dropdown.data('projectId');
const milestonesUrl = $dropdown.data('milestones');
......@@ -52,7 +53,6 @@ export default class MilestoneSelect {
if (issueUpdateURL) {
milestoneLinkTemplate = _.template('<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>');
milestoneLinkNoneTemplate = '<span class="no-value">None</span>';
collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- name %><br /><%- remaining %>" data-placement="left" data-html="true"> <%- title %> </span>');
}
return $dropdown.glDropdown({
showMenuAbove: showMenuAbove,
......@@ -214,10 +214,16 @@ export default class MilestoneSelect {
data.milestone.remaining = timeFor(data.milestone.due_date);
data.milestone.name = data.milestone.title;
$value.html(milestoneLinkTemplate(data.milestone));
return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone));
return $sidebarCollapsedValue
.attr('data-original-title', `${data.milestone.name}<br />${data.milestone.remaining}`)
.find('span')
.text(data.milestone.title);
} else {
$value.html(milestoneLinkNoneTemplate);
return $sidebarCollapsedValue.find('span').text('No');
return $sidebarCollapsedValue
.attr('data-original-title', __('Milestone'))
.find('span')
.text(__('None'));
}
})
.catch(() => {
......
......@@ -1427,7 +1427,7 @@ export default class Notes {
const { discussion_html } = data;
const lines = $(discussion_html).find('.line_holder');
lines.addClass('fade-in');
$container.find('tbody').prepend(lines);
$container.find('.diff-content > table > tbody').prepend(lines);
const fileHolder = $container.find('.file-holder');
$container.find('.line-holder-placeholder').remove();
syntaxHighlight(fileHolder);
......
......@@ -19,7 +19,7 @@ function getSystemDate(systemUtcOffsetSeconds) {
const date = new Date();
const localUtcOffsetMinutes = 0 - date.getTimezoneOffset();
const systemUtcOffsetMinutes = systemUtcOffsetSeconds / 60;
date.setMinutes((date.getMinutes() - localUtcOffsetMinutes) + systemUtcOffsetMinutes);
date.setMinutes(date.getMinutes() - localUtcOffsetMinutes + systemUtcOffsetMinutes);
return date;
}
......@@ -35,18 +35,36 @@ function formatTooltipText({ date, count }) {
return `${contribText}<br />${dateDayName} ${dateText}`;
}
const initColorKey = () => d3.scaleLinear().range(['#acd5f2', '#254e77']).domain([0, 3]);
const initColorKey = () =>
d3
.scaleLinear()
.range(['#acd5f2', '#254e77'])
.domain([0, 3]);
export default class ActivityCalendar {
constructor(container, timestamps, calendarActivitiesPath, utcOffset = 0) {
constructor(container, timestamps, calendarActivitiesPath, utcOffset = 0, firstDayOfWeek = 0) {
this.calendarActivitiesPath = calendarActivitiesPath;
this.clickDay = this.clickDay.bind(this);
this.currentSelectedDate = '';
this.daySpace = 1;
this.daySize = 15;
this.daySizeWithSpace = this.daySize + (this.daySpace * 2);
this.monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
this.daySizeWithSpace = this.daySize + this.daySpace * 2;
this.monthNames = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
];
this.months = [];
this.firstDayOfWeek = firstDayOfWeek;
// Loop through the timestamps to create a group of objects
// The group of objects will be grouped based on the day of the week they are
......@@ -70,7 +88,7 @@ export default class ActivityCalendar {
// Create a new group array if this is the first day of the week
// or if is first object
if ((day === 0 && i !== 0) || i === 0) {
if ((day === this.firstDayOfWeek && i !== 0) || i === 0) {
this.timestampsTmp.push([]);
group += 1;
}
......@@ -109,21 +127,30 @@ export default class ActivityCalendar {
}
renderSvg(container, group) {
const width = ((group + 1) * this.daySizeWithSpace) + this.getExtraWidthPadding(group);
return d3.select(container)
const width = (group + 1) * this.daySizeWithSpace + this.getExtraWidthPadding(group);
return d3
.select(container)
.append('svg')
.attr('width', width)
.attr('height', 167)
.attr('class', 'contrib-calendar');
}
dayYPos(day) {
return this.daySizeWithSpace * ((day + 7 - this.firstDayOfWeek) % 7);
}
renderDays() {
this.svg.selectAll('g').data(this.timestampsTmp).enter().append('g')
this.svg
.selectAll('g')
.data(this.timestampsTmp)
.enter()
.append('g')
.attr('transform', (group, i) => {
_.each(group, (stamp, a) => {
if (a === 0 && stamp.day === 0) {
const month = stamp.date.getMonth();
const x = (this.daySizeWithSpace * i) + 1 + this.daySizeWithSpace;
const x = this.daySizeWithSpace * i + 1 + this.daySizeWithSpace;
const lastMonth = _.last(this.months);
if (
lastMonth == null ||
......@@ -133,19 +160,20 @@ export default class ActivityCalendar {
}
}
});
return `translate(${(this.daySizeWithSpace * i) + 1 + this.daySizeWithSpace}, 18)`;
return `translate(${this.daySizeWithSpace * i + 1 + this.daySizeWithSpace}, 18)`;
})
.selectAll('rect')
.data(stamp => stamp)
.enter()
.append('rect')
.attr('x', '0')
.attr('y', stamp => this.daySizeWithSpace * stamp.day)
.attr('y', stamp => this.dayYPos(stamp.day))
.attr('width', this.daySize)
.attr('height', this.daySize)
.attr('fill', stamp => (
stamp.count !== 0 ? this.color(Math.min(stamp.count, 40)) : '#ededed'
))
.attr(
'fill',
stamp => (stamp.count !== 0 ? this.color(Math.min(stamp.count, 40)) : '#ededed'),
)
.attr('title', stamp => formatTooltipText(stamp))
.attr('class', 'user-contrib-cell js-tooltip')
.attr('data-container', 'body')
......@@ -156,16 +184,19 @@ export default class ActivityCalendar {
const days = [
{
text: 'M',
y: 29 + (this.daySizeWithSpace * 1),
}, {
y: 29 + this.dayYPos(1),
},
{
text: 'W',
y: 29 + (this.daySizeWithSpace * 3),
}, {
y: 29 + this.dayYPos(2),
},
{
text: 'F',
y: 29 + (this.daySizeWithSpace * 5),
y: 29 + this.dayYPos(3),
},
];
this.svg.append('g')
this.svg
.append('g')
.selectAll('text')
.data(days)
.enter()
......@@ -178,7 +209,8 @@ export default class ActivityCalendar {
}
renderMonths() {
this.svg.append('g')
this.svg
.append('g')
.attr('direction', 'ltr')
.selectAll('text')
.data(this.months)
......@@ -191,11 +223,24 @@ export default class ActivityCalendar {
}
renderKey() {
const keyValues = ['no contributions', '1-9 contributions', '10-19 contributions', '20-29 contributions', '30+ contributions'];
const keyColors = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)];
const keyValues = [
'no contributions',
'1-9 contributions',
'10-19 contributions',
'20-29 contributions',
'30+ contributions',
];
const keyColors = [
'#ededed',
this.colorKey(0),
this.colorKey(1),
this.colorKey(2),
this.colorKey(3),
];
this.svg.append('g')
.attr('transform', `translate(18, ${(this.daySizeWithSpace * 8) + 16})`)
this.svg
.append('g')
.attr('transform', `translate(18, ${this.daySizeWithSpace * 8 + 16})`)
.selectAll('rect')
.data(keyColors)
.enter()
......@@ -211,8 +256,17 @@ export default class ActivityCalendar {
}
initColor() {
const colorRange = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)];
return d3.scaleThreshold().domain([0, 10, 20, 30]).range(colorRange);
const colorRange = [
'#ededed',
this.colorKey(0),
this.colorKey(1),
this.colorKey(2),
this.colorKey(3),
];
return d3
.scaleThreshold()
.domain([0, 10, 20, 30])
.range(colorRange);
}
clickDay(stamp) {
......@@ -227,7 +281,8 @@ export default class ActivityCalendar {
$('.user-calendar-activities').html(LOADING_HTML);
axios.get(this.calendarActivitiesPath, {
axios
.get(this.calendarActivitiesPath, {
params: {
date,
},
......
......@@ -5,6 +5,7 @@ import _ from 'underscore';
import Cookies from 'js-cookie';
import flash from './flash';
import axios from './lib/utils/axios_utils';
import { __ } from './locale';
function Sidebar(currentUser) {
this.toggleTodo = this.toggleTodo.bind(this);
......@@ -41,12 +42,14 @@ Sidebar.prototype.addEventListeners = function() {
};
Sidebar.prototype.sidebarToggleClicked = function (e, triggered) {
var $allGutterToggleIcons, $this, $thisIcon;
var $allGutterToggleIcons, $this, isExpanded, tooltipLabel;
e.preventDefault();
$this = $(this);
$thisIcon = $this.find('i');
isExpanded = $this.find('i').hasClass('fa-angle-double-right');
tooltipLabel = isExpanded ? __('Expand sidebar') : __('Collapse sidebar');
$allGutterToggleIcons = $('.js-sidebar-toggle i');
if ($thisIcon.hasClass('fa-angle-double-right')) {
if (isExpanded) {
$allGutterToggleIcons.removeClass('fa-angle-double-right').addClass('fa-angle-double-left');
$('aside.right-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
$('.layout-page').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
......@@ -57,6 +60,9 @@ Sidebar.prototype.sidebarToggleClicked = function (e, triggered) {
if (gl.lazyLoader) gl.lazyLoader.loadCheck();
}
$this.attr('data-original-title', tooltipLabel);
if (!triggered) {
Cookies.set("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed'));
}
......
<script>
import { __ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
name: 'Assignees',
directives: {
tooltip,
},
props: {
rootPath: {
type: String,
......@@ -14,6 +20,11 @@ export default {
type: Boolean,
required: true,
},
issuableType: {
type: String,
require: true,
default: 'issue',
},
},
data() {
return {
......@@ -62,6 +73,12 @@ export default {
names.push(`+ ${this.users.length - maxRender} more`);
}
if (!this.users.length) {
const emptyTooltipLabel = this.issuableType === 'issue' ?
__('Assignee(s)') : __('Assignee');
names.push(emptyTooltipLabel);
}
return names.join(', ');
},
sidebarAvatarCounter() {
......@@ -109,7 +126,8 @@ export default {
<div>
<div
class="sidebar-collapsed-icon sidebar-collapsed-user"
:class="{ 'multiple-users': hasMoreThanOneAssignee, 'has-tooltip': hasAssignees }"
:class="{ 'multiple-users': hasMoreThanOneAssignee }"
v-tooltip
data-container="body"
data-placement="left"
:title="collapsedTooltipTitle"
......
<script>
import Flash from '../../../flash';
import Flash from '~/flash';
import eventHub from '~/sidebar/event_hub';
import Store from '~/sidebar/stores/sidebar_store';
import AssigneeTitle from './assignee_title.vue';
import Assignees from './assignees.vue';
import Store from '../../stores/sidebar_store';
import eventHub from '../../event_hub';
export default {
name: 'SidebarAssignees',
......@@ -25,6 +25,11 @@ export default {
required: false,
default: false,
},
issuableType: {
type: String,
require: true,
default: 'issue',
},
},
data() {
return {
......@@ -90,6 +95,7 @@ export default {
:users="store.assignees"
:editable="store.editable"
@assign-self="assignSelf"
:issuable-type="issuableType"
/>
</div>
</template>
<script>
import Flash from '../../../flash';
import { __ } from '~/locale';
import Flash from '~/flash';
import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '~/sidebar/event_hub';
import editForm from './edit_form.vue';
import Icon from '../../../vue_shared/components/icon.vue';
import { __ } from '../../../locale';
import eventHub from '../../event_hub';
export default {
components: {
editForm,
Icon,
},
directives: {
tooltip,
},
props: {
isConfidential: {
required: true,
......@@ -33,6 +37,9 @@ export default {
confidentialityIcon() {
return this.isConfidential ? 'eye-slash' : 'eye';
},
tooltipLabel() {
return this.isConfidential ? __('Confidential') : __('Not confidential');
},
},
created() {
eventHub.$on('closeConfidentialityForm', this.toggleForm);
......@@ -65,6 +72,10 @@ export default {
<div
class="sidebar-collapsed-icon"
@click="toggleForm"
v-tooltip
data-container="body"
data-placement="left"
:title="tooltipLabel"
>
<icon
:name="confidentialityIcon"
......
<script>
import { __ } from '~/locale';
import Flash from '~/flash';
import tooltip from '~/vue_shared/directives/tooltip';
import issuableMixin from '~/vue_shared/mixins/issuable';
import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '~/sidebar/event_hub';
import editForm from './edit_form.vue';
import issuableMixin from '../../../vue_shared/mixins/issuable';
import Icon from '../../../vue_shared/components/icon.vue';
import eventHub from '../../event_hub';
export default {
components: {
editForm,
Icon,
},
directives: {
tooltip,
},
mixins: [issuableMixin],
props: {
......@@ -44,6 +51,10 @@ export default {
isLockDialogOpen() {
return this.mediator.store.isLockDialogOpen;
},
tooltipLabel() {
return this.isLocked ? __('Locked') : __('Unlocked');
},
},
created() {
......@@ -85,6 +96,10 @@ export default {
<div
class="sidebar-collapsed-icon"
@click="toggleForm"
v-tooltip
data-container="body"
data-placement="left"
:title="tooltipLabel"
>
<icon
:name="lockIcon"
......
<script>
import { __, n__, sprintf } from '../../../locale';
import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
import userAvatarImage from '../../../vue_shared/components/user_avatar/user_avatar_image.vue';
import { __, n__, sprintf } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import userAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
export default {
directives: {
tooltip,
},
components: {
loadingIcon,
userAvatarImage,
......@@ -72,7 +76,13 @@
<template>
<div>
<div class="sidebar-collapsed-icon">
<div
class="sidebar-collapsed-icon"
v-tooltip
data-container="body"
data-placement="left"
:title="participantLabel"
>
<i
class="fa fa-users"
aria-hidden="true"
......
<script>
import icon from '../../../vue_shared/components/icon.vue';
import { abbreviateTime } from '../../../lib/utils/pretty_time';
import { __, sprintf } from '~/locale';
import { abbreviateTime } from '~/lib/utils/pretty_time';
import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
name: 'TimeTrackingCollapsedState',
components: {
icon,
},
directives: {
tooltip,
},
props: {
showComparisonState: {
type: Boolean,
......@@ -79,6 +84,21 @@
return '';
},
timeTrackedTooltipText() {
let title;
if (this.showComparisonState) {
title = __('Time remaining');
} else if (this.showEstimateOnlyState) {
title = __('Estimated');
} else if (this.showSpentOnlyState) {
title = __('Time spent');
}
return sprintf('%{title}: %{text}', ({ title, text: this.text }));
},
tooltipText() {
return this.showNoTimeTrackingState ? __('Time tracking') : this.timeTrackedTooltipText;
},
},
methods: {
abbreviateTime(timeStr) {
......@@ -89,7 +109,13 @@
</script>
<template>
<div class="sidebar-collapsed-icon">
<div
class="sidebar-collapsed-icon"
v-tooltip
data-container="body"
data-placement="left"
:title="tooltipText"
>
<icon name="timer" />
<div class="time-tracking-collapsed-summary">
<div :class="divClass">
......
......@@ -27,6 +27,7 @@ function mountAssigneesComponent(mediator) {
mediator,
field: el.dataset.field,
signedIn: el.hasAttribute('data-signed-in'),
issuableType: gl.utils.isInIssuePage() ? 'issue' : 'merge_request',
},
}),
});
......
......@@ -5,6 +5,7 @@
import $ from 'jquery';
import _ from 'underscore';
import axios from './lib/utils/axios_utils';
import { __ } from './locale';
import ModalStore from './boards/stores/modal_store';
// TODO: remove eventHub hack after code splitting refactor
......@@ -182,7 +183,7 @@ function UsersSelect(currentUser, els, options = {}) {
return axios.put(issueURL, data)
.then(({ data }) => {
var user;
var user, tooltipTitle;
$dropdown.trigger('loaded.gl.dropdown');
$loading.fadeOut();
if (data.assignee) {
......@@ -191,15 +192,17 @@ function UsersSelect(currentUser, els, options = {}) {
username: data.assignee.username,
avatar: data.assignee.avatar_url
};
tooltipTitle = _.escape(user.name);
} else {
user = {
name: 'Unassigned',
username: '',
avatar: ''
};
tooltipTitle = __('Assignee');
}
$value.html(assigneeTemplate(user));
$collapsedSidebar.attr('title', _.escape(user.name)).tooltip('fixTitle');
$collapsedSidebar.attr('title', tooltipTitle).tooltip('fixTitle');
return $collapsedSidebar.html(collapsedAssigneeTemplate(user));
});
};
......
<script>
export default {
import { __ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
name: 'ToggleSidebar',
directives: {
tooltip,
},
props: {
collapsed: {
type: Boolean,
required: true,
},
},
computed: {
tooltipLabel() {
return this.collapsed ? __('Expand sidebar') : __('Collapse sidebar');
},
},
methods: {
toggle() {
this.$emit('toggle');
},
},
};
};
</script>
<template>
......@@ -20,6 +31,10 @@
type="button"
class="btn btn-blank gutter-toggle btn-sidebar-action"
@click="toggle"
v-tooltip
data-container="body"
data-placement="left"
:title="tooltipLabel"
>
<i
aria-label="toggle collapse"
......
......@@ -247,6 +247,7 @@ $btn-sm-side-margin: 7px;
$btn-xs-side-margin: 5px;
$issue-status-expired: $orange-500;
$issuable-sidebar-color: $gl-text-color-secondary;
$sidebar-block-hover-color: #ebebeb;
$group-path-color: #999;
$namespace-kind-color: #aaa;
$panel-heading-link-color: #777;
......@@ -373,6 +374,8 @@ $dropdown-hover-color: $blue-400;
$link-active-background: rgba(0, 0, 0, 0.04);
$link-hover-background: rgba(0, 0, 0, 0.06);
$inactive-badge-background: rgba(0, 0, 0, 0.08);
$sidebar-toggle-height: 60px;
$sidebar-milestone-toggle-bottom-margin: 10px;
/*
* Buttons
......
......@@ -187,7 +187,12 @@
padding-left: 10px;
&:hover {
color: $gray-darkest;
color: $gl-text-color;
}
&:hover,
&:focus {
text-decoration: none;
}
}
......@@ -368,6 +373,14 @@
padding: 15px 0 0;
border-bottom: 0;
overflow: hidden;
&:hover {
background-color: $sidebar-block-hover-color;
}
&.issuable-sidebar-header {
padding-top: 0;
}
}
.participants {
......@@ -380,8 +393,17 @@
.gutter-toggle {
width: 100%;
height: $sidebar-toggle-height;
margin-left: 0;
padding-left: 25px;
padding-left: 0;
border-bottom: 1px solid $border-gray-dark;
}
a.gutter-toggle {
display: flex;
justify-content: center;
flex-direction: column;
text-align: center;
}
.sidebar-collapsed-icon {
......@@ -428,10 +450,10 @@
.btn-clipboard {
border: 0;
background: transparent;
color: $issuable-sidebar-color;
&:hover {
background: transparent;
color: $gl-text-color;
}
}
......
......@@ -53,10 +53,6 @@
}
.milestone-sidebar {
.gutter-toggle {
margin-bottom: 10px;
}
.milestone-progress {
.title {
padding-top: 5px;
......@@ -102,7 +98,17 @@
margin-right: 0;
}
.right-sidebar-expanded & {
.gutter-toggle {
margin-bottom: $sidebar-milestone-toggle-bottom-margin;
}
}
.right-sidebar-collapsed & {
.milestone-progress {
padding-top: 0;
}
.reference {
border-top: 1px solid $border-gray-normal;
}
......
......@@ -134,11 +134,11 @@ class Projects::IssuesController < Projects::ApplicationController
def can_create_branch
can_create = current_user &&
can?(current_user, :push_code, @project) &&
@issue.can_be_worked_on?(current_user)
@issue.can_be_worked_on?
respond_to do |format|
format.json do
render json: { can_create_branch: can_create, has_related_branch: @issue.has_related_branch? }
render json: { can_create_branch: can_create, suggested_branch_name: @issue.suggested_branch_name }
end
end
end
......@@ -177,7 +177,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
def authorize_create_merge_request!
render_404 unless can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user)
render_404 unless can?(current_user, :push_code, @project) && @issue.can_be_worked_on?
end
def render_issue_json
......
......@@ -28,11 +28,12 @@ class Projects::RepositoriesController < Projects::ApplicationController
end
def assign_archive_vars
@id = params[:id]
return unless @id
@ref, @filename = extract_ref(@id)
if params[:id]
@ref, @filename = extract_ref(params[:id])
else
@ref = params[:ref]
@filename = nil
end
rescue InvalidPathError
render_404
end
......
......@@ -9,6 +9,32 @@ module IssuablesHelper
"right-sidebar-#{sidebar_gutter_collapsed? ? 'collapsed' : 'expanded'}"
end
def sidebar_gutter_tooltip_text
sidebar_gutter_collapsed? ? _('Expand sidebar') : _('Collapse sidebar')
end
def sidebar_assignee_tooltip_label(issuable)
if issuable.assignee
issuable.assignee.name
else
issuable.allows_multiple_assignees? ? _('Assignee(s)') : _('Assignee')
end
end
def sidebar_due_date_tooltip_label(issuable)
if issuable.due_date
"#{_('Due date')}<br />#{due_date_remaining_days(issuable)}"
else
_('Due date')
end
end
def due_date_remaining_days(issuable)
remaining_days_in_words = remaining_days_in_words(issuable)
"#{issuable.due_date.to_s(:medium)} (#{remaining_days_in_words})"
end
def multi_label_name(current_labels, default_label)
if current_labels && current_labels.any?
title = current_labels.first.try(:title)
......@@ -153,10 +179,14 @@ module IssuablesHelper
def issuable_labels_tooltip(labels, limit: 5)
first, last = labels.partition.with_index { |_, i| i < limit }
if labels && labels.any?
label_names = first.collect(&:name)
label_names << "and #{last.size} more" unless last.empty?
label_names.join(', ')
else
_("Labels")
end
end
def issuables_state_counter_text(issuable_type, state, display_count)
......@@ -321,7 +351,7 @@ module IssuablesHelper
def issuable_todo_button_data(issuable, todo, is_collapsed)
{
todo_text: "Add todo",
mark_text: "Mark done",
mark_text: "Mark todo as done",
todo_icon: (is_collapsed ? icon('plus-square') : nil),
mark_icon: (is_collapsed ? icon('check-square', class: 'todo-undone') : nil),
issuable_id: issuable.id,
......
module MilestonesHelper
include EntityDateHelper
def milestones_filter_path(opts = {})
if @project
project_milestones_path(@project, opts)
......@@ -72,6 +74,19 @@ module MilestonesHelper
end
end
def milestone_progress_tooltip_text(milestone)
has_issues = milestone.total_issues_count(current_user) > 0
if has_issues
[
_('Progress'),
_("%{percent}%% complete") % { percent: milestone.percent_complete(current_user) }
].join('<br />')
else
_('Progress')
end
end
def milestone_progress_bar(milestone)
options = {
class: 'progress-bar progress-bar-success',
......@@ -95,27 +110,69 @@ module MilestonesHelper
end
def milestone_tooltip_title(milestone)
if milestone.due_date
[milestone.due_date.to_s(:medium), "(#{milestone_remaining_days(milestone)})"].join(' ')
if milestone
"#{milestone.title}<br />#{milestone_tooltip_due_date(milestone)}"
else
_('Milestone')
end
end
def milestone_remaining_days(milestone)
if milestone.expired?
content_tag(:strong, 'Past due')
elsif milestone.upcoming?
content_tag(:strong, 'Upcoming')
elsif milestone.due_date
time_ago = time_ago_in_words(milestone.due_date)
content = time_ago.gsub(/\d+/) { |match| "<strong>#{match}</strong>" }
content.slice!("about ")
content << " remaining"
content.html_safe
elsif milestone.start_date && milestone.start_date.past?
days = milestone.elapsed_days
content = content_tag(:strong, days)
content << " #{'day'.pluralize(days)} elapsed"
def milestone_time_for(date, date_type)
title = date_type == :start ? "Start date" : "End date"
if date
time_ago = time_ago_in_words(date)
time_ago.slice!("about ")
time_ago << if date.past?
" ago"
else
" remaining"
end
content = [
title,
"<br />",
date.to_s(:medium),
"(#{time_ago})"
].join(" ")
content.html_safe
else
title
end
end
def milestone_issues_tooltip_text(milestone)
issues = milestone.count_issues_by_state(current_user)
return _("Issues") if issues.empty?
content = []
content << n_("1 open issue", "%d open issues", issues["opened"]) % issues["opened"] if issues["opened"]
content << n_("1 closed issue", "%d closed issues", issues["closed"]) % issues["closed"] if issues["closed"]
content.join('<br />').html_safe
end
def milestone_merge_requests_tooltip_text(milestone)
merge_requests = milestone.merge_requests
return _("Merge requests") if merge_requests.empty?
content = []
content << n_("1 open merge request", "%d open merge requests", merge_requests.opened.count) % merge_requests.opened.count if merge_requests.opened.any?
content << n_("1 closed merge request", "%d closed merge requests", merge_requests.closed.count) % merge_requests.closed.count if merge_requests.closed.any?
content << n_("1 merged merge request", "%d merged merge requests", merge_requests.merged.count) % merge_requests.merged.count if merge_requests.merged.any?
content.join('<br />').html_safe
end
def milestone_tooltip_due_date(milestone)
if milestone.due_date
"#{milestone.due_date.to_s(:medium)} (#{remaining_days_in_words(milestone)})"
end
end
......
......@@ -248,7 +248,7 @@ class Commit
end
def notes_with_associations
notes.includes(:author)
notes.includes(:author, :award_emoji)
end
def merge_requests
......
......@@ -102,14 +102,14 @@ module Milestoneish
Gitlab::TimeTrackingFormatter.output(total_issue_time_estimate)
end
private
def count_issues_by_state(user)
memoize_per_user(user, :count_issues_by_state) do
issues_visible_to_user(user).reorder(nil).group(:state).count
end
end
private
def memoize_per_user(user, method_name)
memoized_users[method_name][user&.id] ||= yield
end
......
# Uniquify
#
# Return a version of the given 'base' string that is unique
# by appending a counter to it. Uniqueness is determined by
# repeated calls to the passed block.
#
# You can pass an initial value for the counter, if not given
# counting starts from 1.
#
# If `base` is a function/proc, we expect that calling it with a
# candidate counter returns a string to test/return.
class Uniquify
# Return a version of the given 'base' string that is unique
# by appending a counter to it. Uniqueness is determined by
# repeated calls to the passed block.
#
# If `base` is a function/proc, we expect that calling it with a
# candidate counter returns a string to test/return.
def initialize(counter = nil)
@counter = counter
end
def string(base)
@base = base
@counter = nil
increment_counter! while yield(base_string)
base_string
......
......@@ -194,6 +194,15 @@ class Issue < ActiveRecord::Base
branches_with_iid - branches_with_merge_request
end
def suggested_branch_name
return to_branch_name unless project.repository.branch_exists?(to_branch_name)
start_counting_from = 2
Uniquify.new(start_counting_from).string(-> (counter) { "#{to_branch_name}-#{counter}" }) do |suggested_branch_name|
project.repository.branch_exists?(suggested_branch_name)
end
end
# Returns boolean if a related branch exists for the current issue
# ignores merge requests branchs
def has_related_branch?
......@@ -248,11 +257,8 @@ class Issue < ActiveRecord::Base
end
end
def can_be_worked_on?(current_user)
!self.closed? &&
!self.project.forked? &&
self.related_branches(current_user).empty? &&
self.closed_by_merge_requests(current_user).empty?
def can_be_worked_on?
!self.closed? && !self.project.forked?
end
# Returns `true` if the current issue can be viewed by either a logged in User
......
......@@ -947,10 +947,13 @@ class User < ActiveRecord::Base
end
def manageable_groups
union = Gitlab::SQL::Union.new([owned_groups.select(:id),
masters_groups.select(:id)])
arel_union = Arel::Nodes::SqlLiteral.new(union.to_sql)
owned_and_master_groups = Group.where(Group.arel_table[:id].in(arel_union))
union_sql = Gitlab::SQL::Union.new([owned_groups.select(:id), masters_groups.select(:id)]).to_sql
# Update this line to not use raw SQL when migrated to Rails 5.2.
# Either ActiveRecord or Arel constructions are fine.
# This was replaced with the raw SQL construction because of bugs in the arel gem.
# Bugs were fixed in arel 9.0.0 (Rails 5.2).
owned_and_master_groups = Group.where("namespaces.id IN (#{union_sql})") # rubocop:disable GitlabSecurity/SqlInjection
Gitlab::GroupHierarchy.new(owned_and_master_groups).base_and_descendants
end
......
module EntityDateHelper
include ActionView::Helpers::DateHelper
include ActionView::Helpers::TagHelper
def interval_in_words(diff)
return 'Not started' unless diff
......@@ -34,4 +35,30 @@ module EntityDateHelper
duration_hash
end
# Generates an HTML-formatted string for remaining dates based on start_date and due_date
#
# It returns "Past due" for expired entities
# It returns "Upcoming" for upcoming entities
# If due date is provided, it returns "# days|weeks|months remaining|ago"
# If start date is provided and elapsed, with no due date, it returns "# days elapsed"
def remaining_days_in_words(entity)
if entity.try(:expired?)
content_tag(:strong, 'Past due')
elsif entity.try(:upcoming?)
content_tag(:strong, 'Upcoming')
elsif entity.due_date
is_upcoming = (entity.due_date - Date.today).to_i > 0
time_ago = time_ago_in_words(entity.due_date)
content = time_ago.gsub(/\d+/) { |match| "<strong>#{match}</strong>" }
content.slice!("about ")
content << " " + (is_upcoming ? _("remaining") : _("ago"))
content.html_safe
elsif entity.start_date && entity.start_date.past?
days = entity.elapsed_days
content = content_tag(:strong, days)
content << " #{'day'.pluralize(days)} elapsed"
content.html_safe
end
end
end
......@@ -8,9 +8,10 @@ module Projects
template_name = params.delete(:template_name)
file = Gitlab::ProjectTemplate.find(template_name).file
override_params = params.dup
params[:file] = file
GitlabProjectsImportService.new(current_user, params).execute
GitlabProjectsImportService.new(current_user, params, override_params).execute
ensure
file&.close
......
......@@ -26,7 +26,7 @@
- if issue.milestone
%span.issuable-milestone.hidden-xs
&nbsp;
= link_to project_issues_path(issue.project, milestone_title: issue.milestone.title), data: { html: 1, toggle: 'tooltip', title: issuable_milestone_tooltip_title(issue) } do
= link_to project_issues_path(issue.project, milestone_title: issue.milestone.title), data: { html: 1, toggle: 'tooltip', title: milestone_tooltip_due_date(issue.milestone) } do
= icon('clock-o')
= issue.milestone.title
- if issue.due_date
......
......@@ -23,7 +23,7 @@
- if merge_request.milestone
%span.issuable-milestone.hidden-xs
&nbsp;
= link_to project_merge_requests_path(merge_request.project, milestone_title: merge_request.milestone.title), data: { html: 1, toggle: 'tooltip', title: issuable_milestone_tooltip_title(merge_request) } do
= link_to project_merge_requests_path(merge_request.project, milestone_title: merge_request.milestone.title), data: { html: 1, toggle: 'tooltip', title: milestone_tooltip_due_date(merge_request.milestone) } do
= icon('clock-o')
= merge_request.milestone.title
- if merge_request.target_project.default_branch != merge_request.target_branch
......
......@@ -7,7 +7,7 @@
- if current_user
%span.issuable-header-text.hide-collapsed.pull-left
= _('Todo')
%a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => "Toggle sidebar" }
%a.gutter-toggle.pull-right.js-sidebar-toggle.has-tooltip{ role: "button", href: "#", "aria-label" => "Toggle sidebar", title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left' } }
= sidebar_gutter_toggle_icon
- if current_user
= render "shared/issuable/sidebar_todo", todo: todo, issuable: issuable
......@@ -19,11 +19,10 @@
.block.assignee
= render "shared/issuable/sidebar_assignees", issuable: issuable, can_edit_issuable: can_edit_issuable, signed_in: current_user.present?
.block.milestone
.sidebar-collapsed-icon
.sidebar-collapsed-icon.has-tooltip{ title: milestone_tooltip_title(issuable.milestone), data: { container: 'body', html: 1, placement: 'left' } }
= icon('clock-o', 'aria-hidden': 'true')
%span.milestone-title
- if issuable.milestone
%span.has-tooltip{ title: "#{issuable.milestone.title}<br>#{milestone_tooltip_title(issuable.milestone)}", data: { container: 'body', html: 1, placement: 'left' } }
= issuable.milestone.title
- else
= _('None')
......@@ -34,7 +33,7 @@
= link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
.value.hide-collapsed
- if issuable.milestone
= link_to issuable.milestone.title, milestone_path(issuable.milestone), class: "bold has-tooltip", title: milestone_tooltip_title(issuable.milestone), data: { container: "body", html: 1 }
= link_to issuable.milestone.title, milestone_path(issuable.milestone), class: "bold has-tooltip", title: milestone_tooltip_due_date(issuable.milestone), data: { container: "body", html: 1 }
- else
%span.no-value
= _('None')
......@@ -50,7 +49,7 @@
= icon('spinner spin', 'aria-hidden': 'true')
- if issuable.has_attribute?(:due_date)
.block.due_date
.sidebar-collapsed-icon
.sidebar-collapsed-icon.has-tooltip{ data: { placement: 'left', container: 'body', html: 1 }, title: sidebar_due_date_tooltip_label(issuable) }
= icon('calendar', 'aria-hidden': 'true')
%span.js-due-date-sidebar-value
= issuable.due_date.try(:to_s, :medium) || 'None'
......
......@@ -4,7 +4,7 @@
= _('Assignee')
= icon('spinner spin')
- else
.sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: (issuable.assignee.name if issuable.assignee) }
.sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: sidebar_assignee_tooltip_label(issuable) }
- if issuable.assignee
= link_to_member(@project, issuable.assignee, size: 24)
- else
......
- is_collapsed = local_assigns.fetch(:is_collapsed, false)
- mark_content = is_collapsed ? icon('check-square', class: 'todo-undone') : _('Mark done')
- mark_content = is_collapsed ? icon('check-square', class: 'todo-undone') : _('Mark todo as done')
- todo_content = is_collapsed ? icon('plus-square') : _('Add todo')
%button.issuable-todo-btn.js-issuable-todo{ type: 'button',
class: (is_collapsed ? 'btn-blank sidebar-collapsed-icon dont-change-state has-tooltip' : 'btn btn-default issuable-header-btn pull-right'),
title: (todo.nil? ? _('Add todo') : _('Mark done')),
'aria-label' => (todo.nil? ? _('Add todo') : _('Mark done')),
title: (todo.nil? ? _('Add todo') : _('Mark todo as done')),
'aria-label' => (todo.nil? ? _('Add todo') : _('Mark todo as done')),
data: issuable_todo_button_data(issuable, todo, is_collapsed) }
%span.issuable-todo-inner.js-issuable-todo-inner<
- if todo
......
- merge_request = issuable
.block.assignee
.sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: (merge_request.assignee.name if merge_request.assignee) }
.sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: sidebar_assignee_tooltip_label(issuable) }
- if merge_request.assignee
= link_to_member(@project, merge_request.assignee, size: 24)
- else
......
......@@ -4,12 +4,8 @@
%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => affix_offset, "spy" => "affix", "always-show-toggle" => true }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
.issuable-sidebar.milestone-sidebar
.block.milestone-progress.issuable-sidebar-header
%a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => "Toggle sidebar" }
%a.gutter-toggle.pull-right.js-sidebar-toggle.has-tooltip{ role: "button", href: "#", "aria-label" => "Toggle sidebar", title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left' } }
= sidebar_gutter_toggle_icon
.sidebar-collapsed-icon
%span== #{milestone.percent_complete(current_user)}%
= milestone_progress_bar(milestone)
.title.hide-collapsed
%strong.bold== #{milestone.percent_complete(current_user)}%
%span.hide-collapsed
......@@ -17,6 +13,11 @@
.value.hide-collapsed
= milestone_progress_bar(milestone)
.block.milestone-progress.hide-expanded
.sidebar-collapsed-icon.has-tooltip{ title: milestone_progress_tooltip_text(milestone), data: { container: 'body', html: 1, placement: 'left' } }
%span== #{milestone.percent_complete(current_user)}%
= milestone_progress_bar(milestone)
.block.start_date.hide-collapsed
.title
Start date
......@@ -35,18 +36,24 @@
%span.collapsed-milestone-date
- if milestone.start_date && milestone.due_date
- if milestone.start_date.year == milestone.due_date.year
.milestone-date= milestone.start_date.strftime('%b %-d')
.milestone-date.has-tooltip{ title: milestone_time_for(milestone.start_date, :start), data: { container: 'body', html: 1, placement: 'left' } }
= milestone.start_date.strftime('%b %-d')
- else
.milestone-date= milestone.start_date.strftime('%b %-d %Y')
.milestone-date.has-tooltip{ title: milestone_time_for(milestone.start_date, :start), data: { container: 'body', html: 1, placement: 'left' } }
= milestone.start_date.strftime('%b %-d %Y')
.date-separator -
.due_date= milestone.due_date.strftime('%b %-d %Y')
.due_date.has-tooltip{ title: milestone_time_for(milestone.due_date, :end), data: { container: 'body', html: 1, placement: 'left' } }
= milestone.due_date.strftime('%b %-d %Y')
- elsif milestone.start_date
From
.milestone-date= milestone.start_date.strftime('%b %-d %Y')
.milestone-date.has-tooltip{ title: milestone_time_for(milestone.start_date, :start), data: { container: 'body', html: 1, placement: 'left' } }
= milestone.start_date.strftime('%b %-d %Y')
- elsif milestone.due_date
Until
.milestone-date= milestone.due_date.strftime('%b %-d %Y')
.milestone-date.has-tooltip{ title: milestone_time_for(milestone.due_date, :end), data: { container: 'body', html: 1, placement: 'left' } }
= milestone.due_date.strftime('%b %-d %Y')
- else
.has-tooltip{ title: milestone_time_for(milestone.start_date, :start), data: { container: 'body', html: 1, placement: 'left' } }
None
.title.hide-collapsed
Due date
......@@ -58,14 +65,14 @@
%span.bold= milestone.due_date.to_s(:medium)
- else
%span.no-value No due date
- remaining_days = milestone_remaining_days(milestone)
- remaining_days = remaining_days_in_words(milestone)
- if remaining_days.present?
= surround '(', ')' do
%span.remaining-days= remaining_days
- if !project || can?(current_user, :read_issue, project)
.block.issues
.sidebar-collapsed-icon
.sidebar-collapsed-icon.has-tooltip{ title: milestone_issues_tooltip_text(milestone), data: { container: 'body', html: 1, placement: 'left' } }
%strong
= custom_icon('issues')
%span= milestone.issues_visible_to_user(current_user).count
......@@ -93,7 +100,7 @@
= icon('spinner spin')
.block.merge-requests
.sidebar-collapsed-icon
.sidebar-collapsed-icon.has-tooltip{ title: milestone_merge_requests_tooltip_text(milestone), data: { container: 'body', html: 1, placement: 'left' } }
%strong
= custom_icon('mr_bold')
%span= milestone.merge_requests.count
......
---
title: Enable restore rake task to handle nested storage directories
merge_request: 17516
author: Balasankar C
type: fixed
---
title: Add support for patch link extension for commit links on GitLab Flavored Markdown
merge_request:
author:
type: added
---
title: Include matching branches and tags in protected branches / tags count
merge_request:
author: Jan Beckmann
type: fixed
---
title: Send notification emails when push to a merge request
merge_request: 7610
author: YarNayar
type: feature
---
title: Improve tooltips in collapsed right sidebar
merge_request: 17714
author:
type: changed
---
title: Atomic generation of internal ids for issues.
merge_request: 17580
author:
type: other
---
title: Create Deploy Tokens to allow permanent access to repository and registry
merge_request: 17894
author:
type: added
---
title: Drop JSON response in Project Milestone along with avoiding error
merge_request: 17977
author: Takuya Noguchi
type: fixed
---
title: Fix generated URL when listing repoitories for import
merge_request: 17692
author:
type: fixed
---
title: Fixed bug in dropdown selector when selecting the same selection again
merge_request: 14631
author: bitsapien
type: fixed
---
title: Add an API endpoint to download git repository snapshots
merge_request: 18173
author:
type: added
---
title: Apply NestingDepth (level 5) (framework/dropdowns.scss)
merge_request: 17820
author: Takuya Noguchi
type: other
---
title: 'API: Add parameter merge_method to projects'
merge_request: 18031
author: Jan Beckmann
type: added
---
title: Add object storage support for LFS objects, CI artifacts, and uploads.
merge_request: 17358
author:
type: added
---
title: Increase dropdown width in pipeline graph & center action icon
merge_request: 18089
author:
type: fixed
---
title: 'Introduce simpler env vars for auto devops REPLICAS and CANARY_REPLICAS #41436'
merge_request: 18036
author:
type: added
---
title: Added confirmation modal for changing username
merge_request: 17405
author:
type: added
---
title: Adds the option to the project export API to override the project description and display GitLab export description once imported
merge_request: 17744
author:
type: added
---
title: adds closed by informations in issue api
merge_request: 17042
author: haseebeqx
type: added
---
title: Fix XSS on diff view stored on filenames
merge_request:
author:
type: security
---
title: Long instance urls do not overflow anymore during project creation
merge_request: 17717
author:
type: fixed
---
title: Improved visual styles and consistency for commit hash and possible actions
across commit lists
merge_request: 17406
author:
type: changed
---
title: Improve empty state for canceled job
merge_request: 17646
author:
type: fixed
---
title: Fix hover style of dropdown items in the right sidebar
merge_request: 17519
author:
type: fixed
---
title: Adds cancel btn to new pages domain page
merge_request: 18026
title: Show new branch/mr button even when branch exists
merge_request: 17712
author: Jacopo Beschi @jacopo-beschi
type: added
---
title: Fix Firefox stealing formatting characters on issue notes
merge_request:
author:
type: fixed
---
title: Improve performance of loading issues with lots of references to merge requests
merge_request: 17986
author:
type: performance
---
title: Polish design for verifying domains
merge_request: 17767
author:
type: changed
---
title: Require at least one filter when listing issues or merge requests on dashboard
page
merge_request:
author:
type: performance
---
title: Use specific names for filtered CI variable controller parameters
merge_request: 17796
author:
type: other
---
title: Add empty repo check before running AutoDevOps pipeline
merge_request: 17605
author:
type: changed
---
title: Adds support for OmniAuth JWT provider
merge_request: 17774
author:
type: added
---
title: Limit the number of failed logins when using LDAP for authentication
merge_request: 43525
author:
type: added
---
title: Improves the performance of projects list page
merge_request: 17934
author:
type: performance
---
title: Move ci/lint under project's namespace
merge_request: 17729
author:
type: added
---
title: Update wording to specify create/manage project vs group labels in labels dropdown
merge_request: 17640
author:
type: changed
---
title: Set breadcrumb for admin/runners/show
merge_request: 17431
author: Takuya Noguchi
type: fixed
---
title: Update documentation to reflect current minimum required versions of node and
yarn
merge_request: 17706
author:
type: other
---
title: Store sha256 checksum of artifact metadata
merge_request: 18149
author:
type: added
---
title: Change avatar error message to include allowed file formats
merge_request: 17747
author: Fabian Schneider
type: changed
---
title: Add tooltips to icons in lists of issues and merge requests
merge_request: 17700
author:
type: changed
---
title: Add Gitaly call details to performance bar
merge_request:
author:
type: added
---
title: Update ruby-saml to 1.7.2 and omniauth-saml to 1.10.0
merge_request: 17734
author: Takuya Noguchi
type: security
---
title: Send @mention notifications even if a user has explicitly unsubscribed from
item
merge_request:
author:
type: added
---
title: Implement foreground verification of CI artifacts
merge_request: 17578
author:
type: added
---
title: Fix personal access token clipboard button style
merge_request: 17978
author: Fabian Schneider
type: fixed
---
title: Use singular in the diff stats if only one line has been changed
merge_request: 17697
author: Jan Beckmann
type: fixed
---
title: Use object ID to prevent duplicate keys Vue warning on Issue Boards page during
development
merge_request: 17682
author:
type: other
---
title: Update foreman from 0.78.0 to 0.84.0
merge_request: 17690
author: Takuya Noguchi
type: other
---
title: Stop caching highlighted diffs in Redis unnecessarily
merge_request: 17746
author:
type: performance
---
title: Added i18n support for the prometheus memory widget
merge_request: 17753
author:
type: other
---
title: Update knapsack to 1.16.0
merge_request: 17735
author: Takuya Noguchi
type: other
---
title: Fix viewing diffs on old merge requests
merge_request: 17805
author:
type: fixed
---
title: Display error message on job's tooltip if this one fails
merge_request: 17782
author:
type: added
---
title: Fix search results stripping last endline when parsing the results
merge_request: 17777
author: Jasper Maes
type: fixed
---
title: Add additional cluster usage metrics to usage ping.
merge_request: 17922
author:
type: changed
---
title: Fix UI breakdown for Create merge request button
merge_request: 17821
author: Takuya Noguchi
type: fixed
---
title: Clean up selectors in framework/header.scss
merge_request: 17822
author: Takuya Noguchi
type: other
---
title: Unify format for nested non-task lists
merge_request: 17823
author: Takuya Noguchi
type: fixed
---
title: UX re-design branch items with flexbox
merge_request: 17832
author: Takuya Noguchi
type: fixed
---
title: Update rack-protection to 2.0.1
merge_request: 17835
author: Takuya Noguchi
type: security
---
title: Allow HTTP(s) when git request is made by GitLab CI
merge_request: 18021
author:
type: changed
---
title: Project creation will now raise an error if a service template is invalid
merge_request: 18013
author:
type: fixed
---
title: Fix `gitlab-rake gitlab:two_factor:disable_for_all_users`
merge_request: 18154
author:
type: fixed
---
title: Fix bug rendering group icons when forking
merge_request:
author:
type: fixed
---
title: Reuse root_ref_hash for performance on Branches
merge_request: 17998
author: Takuya Noguchi
type: performance
---
title: Fix `JobsController#raw` endpoint can not read traces in database
merge_request: 18101
author:
type: fixed
---
title: Update asciidoctor-plantuml to 0.0.8
merge_request: 18022
author: Takuya Noguchi
type: performance
---
title: Fixed gitlab:uploads:migrate task ignoring some uploads.
merge_request: 18082
author:
type: fixed
---
title: Fixed gitlab:uploads:migrate task failing for Groups' avatar.
merge_request: 18088
author:
type: fixed
---
title: Update brakeman 3.6.1 to 4.2.1
merge_request: 18122
author: Takuya Noguchi
type: other
---
title: Remove test_ci rake task
merge_request: 18139
author: Takuya Noguchi
type: other
---
title: Fixes unresolved discussions rendering the error state instead of the diff
merge_request:
author:
type: fixed
---
title: Add index to file_store on ci_job_artifacts
merge_request: 18444
author:
type: performance
---
title: Fix specifying a non-default ref when requesting an archive using the legacy
URL
merge_request: 18468
author:
type: fixed
---
title: Always display Labels section in issuable sidebar, even when the project has no labels
merge_request: 18081
author: Branka Martinovic
type: fixed
---
title: Reduce complexity of issuable finder query.
merge_request: 18219
author:
type: performance
---
title: Cache personal projects count.
merge_request: 18197
author:
type: performance
---
title: Remove N+1 query for Noteable association.
merge_request: 17956
author:
type: performance
---
title: Remove unused index from events table.
merge_request: 18014
author:
type: other
---
title: Fix data race between ObjectStorage background_upload and Pages publishing
merge_request:
author:
type: fixed
---
title: Port direct upload of LFS artifacts from EE
merge_request: 17752
author:
type: added
---
title: Add missing port to artifact links
merge_request:
author:
type: fixed
---
title: Add slash command for moving issues
merge_request:
author: Adam Pahlevi
type: added
---
title: Add yellow favicon when `CANARY=true` to differientate canary environment
merge_request: 12477
author:
type: changed
---
title: Add Total CPU/Memory consumption metrics for Kubernetes
merge_request: 17731
author:
type: added
---
title: Update dashboard milestones breadcrumb link
merge_request: 17933
author: George Tsiolis
type: fixed
---
title: Add per-runner configured job timeout
merge_request: 17221
author:
type: added
---
title: Add query counts to profiler output
merge_request:
author:
type: other
---
title: Allow viewing timings for AJAX requests in the performance bar
merge_request:
author:
type: changed
---
title: Bump html-pipeline to 2.7.1
merge_request: 18132
author: "@blackst0ne"
type: other
---
title: Bump `state_machines-activerecord` to 0.5.1
merge_request: 17924
author: blackst0ne
type: other
---
title: Replace the spinach test with an rspec analog
merge_request: 17950
author: blackst0ne
type: other
---
title: Replace the `project/issues/labels.feature` spinach test with an rspec analog
merge_request: 18126
author: blackst0ne
type: other
---
title: Support LFS objects when importing/exporting GitLab project archives
merge_request: 18115
author:
type: added
---
title: Fix importing multiple assignees from GitLab export
merge_request: 17718
author:
type: fixed
---
title: Don't create permanent redirect routes
merge_request: 17521
author:
type: changed
---
title: Allow overriding params on project import through API
merge_request: 18086
author:
type: added
---
title: Use porcelain commit lookup method on CI::CreatePipelineService
merge_request: 17911
author:
type: fixed
---
title: Repository checksum calculation is handled by Gitaly when feature is enabled
merge_request:
author:
type: changed
---
title: Add 'Assigned Issues' and 'Assigned Merge Requests' as dashboard view choices for users
merge_request: 17860
author: Elias Werberich
type: added
---
title: Verify that deploy token has valid access when pulling container registry image
merge_request: 18260
author:
type: fixed
---
title: Allow to store uploads by default on Object Storage
merge_request:
author:
type: added
---
title: Ensure hooks run when a deploy key without a user pushes
merge_request:
author:
type: fixed
---
title: Fix links to subdirectories of a directory with a plus character in its path
merge_request:
author:
type: fixed
---
title: Ensure internal users (ghost, support bot) get assigned a namespace
merge_request:
author:
type: fixed
---
title: Add documentation for Pipelines failure reasons
merge_request: 18352
author:
type: other
---
title: Redesign application settings to match project settings
merge_request: 18019
author:
type: changed
---
title: Escape Markdown characters properly when using autocomplete
merge_request:
author:
type: fixed
---
title: Allow merge requests related to a commit to be found via API
merge_request:
author:
type: added
---
title: Add support for pipeline variables expressions in only/except
merge_request: 17316
author:
type: added
---
title: Detect commit message trailers and link users properly to their accounts
on Gitlab
merge_request: 17919
author: cousine
type: added
---
title: Fix forking to subgroup via API when namespace is given by name
merge_request: 17815
author: Jan Beckmann
type: fixed
---
title: Fix relative uri when "#" is in branch name
merge_request:
author: Jan
type: fixed
---
title: Fix 500 error when a merge request from a fork has conflicts and has not yet
been updated
merge_request:
author:
type: fixed
---
title: Fix GitLab Auth0 integration signing in the wrong user
merge_request:
author:
type: security
---
title: Prioritize weight over title when sorting charts
merge_request: 18233
author:
type: fixed
---
title: Hide emoji popup after multiple spaces
merge_request:
author: Jan Beckmann
type: fixed
---
title: Fixed group deletion linked to Mattermost
merge_request: 16209
author: Julien Millau
type: fixed
---
title: Update no repository placeholder
merge_request: 17964
author: George Tsiolis
type: fixed
---
title: Ignore project internal references in group context
merge_request:
author:
type: fixed
---
title: Fix finding wiki file when Gitaly is enabled
merge_request:
author:
type: fixed
---
title: Fixed some SSRF vulnerabilities in services, hooks and integrations
merge_request: 2337
author:
type: security
---
title: Add better LDAP connection handling
merge_request: 18039
author:
type: fixed
---
title: Extend API for importing a project export with overwrite support
merge_request: 17883
author:
type: added
---
title: Respect visibility options and description when importing project from template
merge_request: 18473
author:
type: fixed
---
title: Extend API for exporting a project with direct upload URL
merge_request: 17686
author:
type: added
---
title: Stop redirecting the page in pipeline main actions
merge_request:
author:
type: fixed
---
title: Added hover background color to IDE file list rows
merge_request:
author:
type: changed
---
title: Fixed IDE button opening the wrong URL in tree list
merge_request:
author:
type: fixed
---
title: Make project avatar in IDE consistent with the rest of GitLab
merge_request:
author:
type: changed
---
title: Increase the memory limits used in the unicorn killer
merge_request: 17948
author:
type: other
---
title: Improve JIRA event descriptions
merge_request:
author:
type: other
---
title: Allow assigning and filtering issuables by ancestor group labels
merge_request:
author:
type: added
---
title: Include subgroup issues when searching for group issues using the API
merge_request:
author:
type: added
---
title: Show issues of subgroups in group-level issue board
merge_request:
author:
type: changed
---
title: Create commit API and Web IDE obey LFS filters
merge_request: 16718
author:
type: fixed
---
title: Adds confidential notes channel for Slack/Mattermost
merge_request:
author:
type: security
---
title: Add realtime pipeline status for adding/viewing files
merge_request: 17705
author:
type: other
---
title: Refactored activity calendar
merge_request: 18469
author: Enrico Scholz
type: changed
---
title: Add average and maximum summary statistics to the prometheus dashboard
merge_request: 17921
author:
type: changed
---
title: Display state indicator for issuable references in non-project scope (e.g.
when referencing issuables from group scope).
merge_request:
author:
type: fixed
---
title: Add alternate archive route for simplified packaging
merge_request: 17225
author:
type: added
---
title: Fixes remove source branch checkbox being visible when user cannot remove the
branch
merge_request:
author:
type: changed
---
title: Move email footer info to a single line
merge_request: 17916
author:
type: changed
---
title: Move 'Registry' after 'CI/CD' in project navigation sidebar
merge_request: 18018
author: Elias Werberich
type: changed
---
title: Make /-/ delimiter optional for search endpoints
merge_request:
author:
type: changed
---
title: Render MR commit SHA instead "diffs" when viable
merge_request:
author:
type: added
---
title: Adjust 404's for LegacyDiffNote discussion rendering
merge_request: 18201
author:
type: fixed
---
title: Add HTTPS-only pages
merge_request: 16273
author: rfwatson
type: added
---
title: File uploads in remote storage now support project renaming.
merge_request: 4597
author:
type: fixed
---
title: Don't include lfs_file_locks data in export bundle
merge_request: 18495
author:
type: fixed
---
title: Reduce number of queries when viewing a merge request
merge_request:
author:
type: performance
---
title: Move AssigneeTitle vue component
merge_request: 17397
author: George Tsiolis
type: performance
---
title: Move MemoryGraph and MemoryUsage vue components
merge_request: 17533
author: George Tsiolis
type: performance
---
title: Move NothingToMerge vue component
merge_request: 17544
author: George Tsiolis
type: performance
---
title: Move ShaMismatch vue component
merge_request: 17546
author: George Tsiolis
type: performance
---
title: Move UnresolvedDiscussions vue component
merge_request: 17538
author: George Tsiolis
type: performance
---
title: Move TimeTrackingComparisonPane vue component
merge_request: 17931
author: George Tsiolis
type: performance
---
title: Move TimeTrackingCollapsedState vue component
merge_request: 17399
author: George Tsiolis
type: performance
---
title: Remove support for legacy tar.gz pages artifacts
merge_request: 18090
author:
type: deprecated
---
title: Automatically cleanup stale worktrees and lock files upon a push
merge_request:
author:
type: fixed
---
title: Use the GitLab version as part of the appearances cache key
merge_request:
author:
type: fixed
---
title: lazy load diffs on merge request discussions
title: Fix N+1 queries when loading participants for a commit note
merge_request:
author:
type: performance
---
title: Add support for Sidekiq JSON logging
merge_request:
author:
type: added
---
title: Memoize Git::Repository#has_visible_content?
merge_request:
author:
type: performance
---
title: Move Sidekiq exporter logs to log/sidekiq_exporter.log
merge_request:
author:
type: other
---
title: Add read-only banner to all pages
merge_request: 17798
author:
type: fixed
---
title: Deleting a MR you are assigned to should decrements counter
merge_request: 17951
author: m b
type: fixed
---
title: Update CI services documnetation
merge_request: 17749
author:
type: other
---
title: Update spec import path for vue mount component helper
merge_request: 17880
author: George Tsiolis
type: performance
---
title: Add i18n and update specs for ShaMismatch vue component
merge_request: 17870
author: George Tsiolis
type: performance
---
title: Use human readable value build_timeout in Project
merge_request: 17386
author:
type: changed
---
title: Projects and groups badges settings UI
merge_request: 17114
author:
type: added
---
title: Rename modal.vue to deprecated_modal.vue
merge_request: 17438
author:
type: other
---
title: Make all workhorse gitaly calls opt-out, take 2
merge_request: 18043
author:
type: other
---
title: Upgrade Gitaly to upgrade its charlock_holmes
merge_request:
author:
type: other
---
title: Allow feature gates to be removed through the API
merge_request:
author:
type: added
---
title: Bulk deleting refs is handled by Gitaly by default
merge_request:
author:
type: performance
---
title: ListCommitsByOid is executed by Gitaly by default
merge_request:
author:
type: performance
---
title: Test if remote repository exists when importing wikis
merge_request:
author:
type: fixed
class AddIndexToCiJobArtifactsFileStore < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :ci_job_artifacts, :file_store
end
def down
remove_index :ci_job_artifacts, :file_store if index_exists?(:ci_job_artifacts, :file_store)
end
end
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20180405142733) do
ActiveRecord::Schema.define(version: 20180418053107) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......@@ -367,6 +367,7 @@ ActiveRecord::Schema.define(version: 20180405142733) do
end
add_index "ci_job_artifacts", ["expire_at", "job_id"], name: "index_ci_job_artifacts_on_expire_at_and_job_id", using: :btree
add_index "ci_job_artifacts", ["file_store"], name: "index_ci_job_artifacts_on_file_store", using: :btree
add_index "ci_job_artifacts", ["job_id", "file_type"], name: "index_ci_job_artifacts_on_job_id_and_file_type", unique: true, using: :btree
add_index "ci_job_artifacts", ["project_id"], name: "index_ci_job_artifacts_on_project_id", using: :btree
......
......@@ -650,6 +650,47 @@ gitlab_rails['redis_sentinels'] = [
Omnibus GitLab configures some things behind the curtains to make the sysadmins'
lives easier. If you want to know what happens underneath keep reading.
### Running multiple Redis clusters
GitLab supports running [separate Redis clusters for different persistent
classes](https://docs.gitlab.com/omnibus/settings/redis.html#running-with-multiple-redis-instances):
cache, queues, and shared_state. To make this work with Sentinel:
1. Set the appropriate variable in `/etc/gitlab/gitlab.rb` for each instance you are using:
```ruby
gitlab_rails['redis_cache_instance'] = REDIS_CACHE_URL
gitlab_rails['redis_queues_instance'] = REDIS_QUEUES_URL
gitlab_rails['redis_shared_state_instance'] = REDIS_SHARED_STATE_URL
```
**Note**: Redis URLs should be in the format: `redis://:PASSWORD@SENTINEL_MASTER_NAME`
1. PASSWORD is the plaintext password for the Redis instance
2. SENTINEL_MASTER_NAME is the Sentinel master name (e.g. `gitlab-redis-cache`)
1. Include an array of hashes with host/port combinations, such as the following:
```ruby
gitlab_rails['redis_cache_sentinels'] = [
{ host: REDIS_CACHE_SENTINEL_HOST, port: PORT1 },
{ host: REDIS_CACHE_SENTINEL_HOST2, port: PORT2 }
]
gitlab_rails['redis_queues_sentinels'] = [
{ host: REDIS_QUEUES_SENTINEL_HOST, port: PORT1 },
{ host: REDIS_QUEUES_SENTINEL_HOST2, port: PORT2 }
]
gitlab_rails['redis_shared_state_sentinels'] = [
{ host: SHARED_STATE_SENTINEL_HOST, port: PORT1 },
{ host: SHARED_STATE_SENTINEL_HOST2, port: PORT2 }
]
```
1. Note that for each persistence class, GitLab will default to using the
configuration specified in `gitlab_rails['redis_sentinels']` unless
overriden by the settings above.
1. Be sure to include BOTH configuration options for each persistent classes. For example,
if you choose to configure a cache instance, you must specify both `gitlab_rails['redis_cache_instance']`
and `gitlab_rails['redis_cache_sentinels']` for GitLab to generate the proper configuration files.
1. Run `gitlab-ctl reconfigure`
### Control running services
In the previous example, we've used `redis_sentinel_role` and
......
......@@ -1399,3 +1399,26 @@ Read more in the [Project Badges](project_badges.md) documentation.
## Issue and merge request description templates
The non-default [issue and merge request description templates](../user/project/description_templates.md) are managed inside the project's repository. So you can manage them via the API through the [Repositories API](repositories.md) and the [Repository Files API](repository_files.md).
## Download snapshot of a git repository
> Introduced in GitLab 10.7
This endpoint may only be accessed by an administrative user.
Download a snapshot of the project (or wiki, if requested) git repository. This
snapshot is always in uncompressed [tar](https://en.wikipedia.org/wiki/Tar_(computing))
format.
If a repository is corrupted to the point where `git clone` does not work, the
snapshot may allow some of the data to be retrieved.
```
GET /projects/:id/snapshot
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `wiki` | boolean | no | Whether to download the wiki, rather than project, repository |
......@@ -31,6 +31,9 @@ sast:container:
- chmod +x clair-scanner
- touch clair-whitelist.yml
- while( ! wget -q -O /dev/null http://docker:6060/v1/namespaces ) ; do sleep 1 ; done
- retries=0
- echo "Waiting for clair daemon to start"
- while( ! wget -T 10 -q -O /dev/null http://docker:6060/v1/namespaces ) ; do sleep 1 ; echo -n "." ; if [ $retries -eq 10 ] ; then echo " Timeout, aborting." ; exit 1 ; fi ; retries=$(($retries+1)) ; done
- ./clair-scanner -c http://docker:6060 --ip $(hostname -i) -r gl-sast-container-report.json -l clair.log -w clair-whitelist.yml ${CI_APPLICATION_REPOSITORY}:${CI_APPLICATION_TAG} || true
artifacts:
paths: [gl-sast-container-report.json]
......
# Frontend Development Process
You can find more about the organization of the frontend team in the [handbook](https://about.gitlab.com/handbook/frontend/).
## Development Checklist
The idea is to remind us about specific topics during the time we build a new feature or start something. This is a common practice in other industries (like pilots) that also use standardised checklists to reduce problems early on.
Copy the content over to your issue or merge request and if something doesn't apply simply remove it from your current list.
This checklist is intended to help us during development of bigger features/refactorings, it's not a "use it always and every point always matches" list.
Please use your best judgement when to use it and please contribute new points through merge requests if something comes to your mind.
---
### Frontend development
#### Planning development
- [ ] Check the current set weight of the issue, does it fit your estimate?
- [ ] Are all [departments](https://about.gitlab.com/handbook/engineering/#engineering-teams) that are needed from your perspective already involved in the issue? (For example is UX missing?)
- [ ] Is the specification complete? Are you missing decisions? How about error handling/defaults/edge cases? Take your time to understand the needed implementation and go through its flow.
- [ ] Are all necessary UX specifications available that you will need in order to implement? Are there new UX components/patterns in the designs? Then contact the UI component team early on. How should error messages or validation be handled?
- [ ] **Library usage** Use Vuex as soon as you have even a medium state to manage, use Vue router if you need to have different views internally and want to link from the outside. Check what libraries we already have for which occassions.
- [ ] **Plan your implementation:**
- [ ] **Architecture plan:** Create a plan aligned with GitLab's architecture, how you are going to do the implementation, for example Vue application setup and its components (through [onion skinning](https://gitlab.com/gitlab-org/gitlab-ce/issues/35873#note_39994091)), Store structure and data flow, which existing Vue components can you reuse. Its a good idea to go through your plan with another engineer to refine it.
- [ ] **Backend:** The best way is to kickoff the implementation in a call and discuss with the assigned Backend engineer what you will need from the backend and also when. Can you reuse existing API's? How is the performance with the planned architecture? Maybe create together a JSON mock object to already start with development.
- [ ] **Communication:** It also makes sense to have for bigger features an own slack channel (normally called #f_{feature_name}) and even weekly demo calls with all people involved.
- [ ] **Dependency Plan:** Are there big dependencies in the plan between you and others, then maybe create an execution diagram to show what is blocking which part and the order of the different parts.
- [ ] **Task list:** Create a simple checklist of the subtasks that are needed for the implementation, also consider creating even sub issues. (for example show a comment, delete a comment, update a comment, etc.). This helps you and also everyone else following the implementation
- [ ] **Keep it small** To make it easier for you and also all reviewers try to keep merge requests small and merge into a feature branch if needed. To accomplish that you need to plan that from the start. Different methods are:
- [ ] **Skeleton based plan** Start with an MR that has the skeleton of the components with placeholder content. In following MRs you can fill the components with interactivity. This also makes it easier to spread out development on multiple people.
- [ ] **Cookie Mode** Think about hiding the feature behind a cookie flag if the implementation is on top of existing features
- [ ] **New route** Are you refactoring something big then you might consider adding a new route where you implement the new feature and when finished delete the current route and rename the new one. (for example 'merge_request' and 'new_merge_request')
- [ ] **Setup** Is there any specific setup needed for your implementation (for example a kubernetes cluster)? Then let everyone know if it is not already mentioned where they can find documentation (if it doesn't exist - create it)
- [ ] **Security** Are there any new security relevant implementations? Then please contact the security team for an app security review. If you are not sure ask our [domain expert](https://about.gitlab.com/handbook/frontend/#frontend-domain-experts)
#### During development
- [ ] Check off tasks on your created task list to keep everyone updated on the progress
- [ ] [Share your work early with reviewers/maintainers](#share-your-work-early)
- [ ] Share your work with UXer and Product Manager with Screenshots and/or [GIF's](https://about.gitlab.com/handbook/product/making-gifs/). They are easy to create for you and keep them up to date.
- [ ] If you are blocked on something let everyone on the issue know through a comment.
- [ ] Are you unable to work on this issue for a longer period of time, also let everyone know.
- [ ] **Documentation** Update/add docs for the new feature, see `docs/`. Ping one of the documentation experts/reviewers
#### Finishing development + Review
- [ ] **Keep it in the scope** Try to focus on the actual scope and avoid a scope creep during review and keep new things to new issues.
- [ ] **Performance** Have you checked performance? For example do the same thing with 500 comments instead of 1. Document the tests and possible findings in the MR so a reviewer can directly see it.
- [ ] Have you tested with a variety of our [supported browsers](../../install/requirements.md#supported-web-browsers)? You can use [browserstack](https://www.browserstack.com/) to be able to access a wide variety of browsers and operating systems.
- [ ] Did you check the mobile view?
- [ ] Check the built webpack bundle (For the report run `WEBPACK_REPORT=true gdk run`, then open `webpack-report/index.html`) if we have unnecessary bloat due to wrong references, including libraries multiple times, etc.. If you need help contact the webpack [domain expert](https://about.gitlab.com/handbook/frontend/#frontend-domain-experts)
- [ ] **Tests** Not only greenfield tests - Test also all bad cases that come to your mind.
- [ ] If you have multiple MR's then also smoke test against the final merge.
- [ ] Are there any big changes on how and especially how frequently we use the API then let production know about it
- [ ] Smoke test of the RC on dev., staging., canary deployments and .com
- [ ] Follow up on issues that came out of the review. Create isssues for discovered edge cases that should be covered in future iterations.
---
### Share your work early
1. Before writing code, ensure your vision of the architecture is aligned with
GitLab's architecture.
1. Add a diagram to the issue and ask a frontend architect in the slack channel `#fe_architectural` about it.
![Diagram of Issue Boards Architecture](img/boards_diagram.png)
1. Don't take more than one week between starting work on a feature and
sharing a Merge Request with a reviewer or a maintainer.
### Vue features
1. Follow the steps in [Vue.js Best Practices](vue.md)
1. Follow the style guide.
1. Only a handful of people are allowed to merge Vue related features.
Reach out to one of Vue experts early in this process.
......@@ -5,11 +5,15 @@ across GitLab's frontend team.
## Overview
GitLab is built on top of [Ruby on Rails][rails] using [Haml][haml] with
[Hamlit][hamlit]. Be wary of [the limitations that come with using
Hamlit][hamlit-limits]. We also use [SCSS][scss] and plain JavaScript with
modern ECMAScript standards supported through [Babel][babel] and ES module
support through [webpack][webpack].
GitLab is built on top of [Ruby on Rails][rails] using [Haml][haml] and also a JavaScript based Frontend with [Vue.js][vue].
Be wary of [the limitations that come with using Hamlit][hamlit-limits]. We also use [SCSS][scss] and plain JavaScript with
modern ECMAScript standards supported through [Babel][babel] and ES module support through [webpack][webpack].
### Javascript development
[Vue.js][vue] is used for particularly advanced, dynamic elements and based on previous iterations [jQuery][jquery] is used in lot of places through the application's JavaScript.
We also use [Axios][axios] to handle all of our network requests.
We also utilize [webpack][webpack] to handle the bundling, minification, and
compression of our assets.
......@@ -18,61 +22,29 @@ Working with our frontend assets requires Node (v6.0 or greater) and Yarn
(v1.2 or greater). You can find information on how to install these on our
[installation guide][install].
[jQuery][jquery] is used throughout the application's JavaScript, with
[Vue.js][vue] for particularly advanced, dynamic elements.
We also use [Axios][axios] to handle all of our network requests.
### Browser Support
For our currently-supported browsers, see our [requirements][requirements].
---
## Development Process
### Share your work early
1. Before writing code guarantee your vision of the architecture is aligned with
GitLab's architecture.
1. Add a diagram to the issue and ask a Frontend Architecture about it.
![Diagram of Issue Boards Architecture](img/boards_diagram.png)
1. Don't take more than one week between starting work on a feature and
sharing a Merge Request with a reviewer or a maintainer.
### Vue features
1. Follow the steps in [Vue.js Best Practices](vue.md)
1. Follow the style guide.
1. Only a handful of people are allowed to merge Vue related features.
Reach out to one of Vue experts early in this process.
---
## [Development Process](development_process.md)
How we plan and execute the work on the frontend.
## [Architecture](architecture.md)
How we go about making fundamental design decisions in GitLab's frontend team
or make changes to our frontend development guidelines.
---
## [Testing](../testing_guide/frontend_testing.md)
How we write frontend tests, run the GitLab test suite, and debug test related
issues.
---
## [Design Patterns](design_patterns.md)
Common JavaScript design patterns in GitLab's codebase.
---
## [Vue.js Best Practices](vue.md)
Vue specific design patterns and practices.
---
## [Axios](axios.md)
Axios specific practices and gotchas.
......
......@@ -524,7 +524,7 @@ export default new Vuex.Store({
_Note:_ If the state of the application is too complex, an individual file for the state may be better.
##### `actions.js`
An action commits a mutatation. In this file, we will write the actions that will call the respective mutation:
An action commits a mutation. In this file, we will write the actions that will commit the respective mutation:
```javascript
import * as types from './mutation_types';
......@@ -661,7 +661,7 @@ describe('component', () => {
};
// populate the store
store.dipatch('addUser', user);
store.dispatch('addUser', user);
vm = new Component({
store,
......
......@@ -67,9 +67,9 @@ and examples in [the `qa/` directory][instance-qa-examples].
## Where can I ask for help?
You can ask question in the `#qa` channel on Slack (GitLab internal) or you can
find an issue you would like to work on in [the issue tracker][gitlab-qa-issues]
and start a new discussion there.
You can ask question in the `#quality` channel on Slack (GitLab internal) or
you can find an issue you would like to work on in
[the issue tracker][gitlab-qa-issues] and start a new discussion there.
[omnibus-gitlab]: https://gitlab.com/gitlab-org/omnibus-gitlab
[gitlab-qa]: https://gitlab.com/gitlab-org/gitlab-qa
......
......@@ -28,7 +28,7 @@ Comments and system notes also appear automatically in response to various actio
#### 2. Todos
- Add todo: add that issue to your [GitLab Todo](../../../workflow/todos.html) list
- Mark done: mark that issue as done (reflects on the Todo list)
- Mark todo as done: mark that issue as done (reflects on the Todo list)
#### 3. Assignee
......
......@@ -70,7 +70,7 @@ In case you want to point a root domain (`example.com`) to your
GitLab Pages site, deployed to `namespace.gitlab.io`, you need to
log into your domain's admin control panel and add a DNS `A` record
pointing your domain to Pages' server IP address. For projects on
GitLab.com, this IP is `52.167.214.135`. For projects leaving in
GitLab.com, this IP is `52.167.214.135`. For projects living in
other GitLab instances (CE or EE), please contact your sysadmin
asking for this information (which IP address is Pages server
running on your instance).
......
......@@ -92,9 +92,9 @@ corresponding **Done** button, and it will disappear from your Todo list.
![A Todo in the Todos dashboard](img/todo_list_item.png)
A Todo can also be marked as done from the issue or merge request sidebar using
the "Mark done" button.
the "Mark todo as done" button.
![Mark Done from the issuable sidebar](img/todos_mark_done_sidebar.png)
![Mark todo as done from the issuable sidebar](img/todos_mark_done_sidebar.png)
You can mark all your Todos as done at once by clicking on the **Mark all as
done** button.
......
......@@ -154,6 +154,7 @@ module API
mount ::API::ProjectHooks
mount ::API::Projects
mount ::API::ProjectMilestones
mount ::API::ProjectSnapshots
mount ::API::ProjectSnippets
mount ::API::ProtectedBranches
mount ::API::Repositories
......
module API
module Helpers
module ProjectSnapshotsHelpers
def authorize_read_git_snapshot!
authenticated_with_full_private_access!
end
def send_git_snapshot(repository)
header(*Gitlab::Workhorse.send_git_snapshot(repository))
end
def snapshot_project
user_project
end
def snapshot_repository
if to_boolean(params[:wiki])
snapshot_project.wiki.repository
else
snapshot_project.repository
end
end
end
end
end
module API
class ProjectSnapshots < Grape::API
helpers ::API::Helpers::ProjectSnapshotsHelpers
before { authorize_read_git_snapshot! }
resource :projects do
desc 'Download a (possibly inconsistent) snapshot of a repository' do
detail 'This feature was introduced in GitLab 10.7'
end
params do
optional :wiki, type: Boolean, desc: 'Set to true to receive the wiki repository'
end
get ':id/snapshot' do
send_git_snapshot(snapshot_repository)
end
end
end
end
......@@ -1258,6 +1258,10 @@ module Gitlab
true
end
def create_from_snapshot(url, auth)
gitaly_repository_client.create_from_snapshot(url, auth)
end
def rebase(user, rebase_id, branch:, branch_sha:, remote_repository:, remote_branch:)
gitaly_migrate(:rebase) do |is_enabled|
if is_enabled
......
......@@ -235,6 +235,22 @@ module Gitlab
)
end
def create_from_snapshot(http_url, http_auth)
request = Gitaly::CreateRepositoryFromSnapshotRequest.new(
repository: @gitaly_repo,
http_url: http_url,
http_auth: http_auth
)
GitalyClient.call(
@storage,
:repository_service,
:create_repository_from_snapshot,
request,
timeout: GitalyClient.default_timeout
)
end
def write_ref(ref_path, ref, old_ref, shell)
request = Gitaly::WriteRefRequest.new(
repository: @gitaly_repo,
......
......@@ -27,8 +27,6 @@ project_tree:
- :releases
- project_members:
- :user
- lfs_file_locks:
- :user
- merge_requests:
- notes:
- :author
......
......@@ -81,6 +81,20 @@ module Gitlab
]
end
def send_git_snapshot(repository)
params = {
'GitalyServer' => gitaly_server_hash(repository),
'GetSnapshotRequest' => Gitaly::GetSnapshotRequest.new(
repository: repository.gitaly_repository
).to_json
}
[
SEND_DATA_HEADER,
"git-snapshot:#{encode(params)}"
]
end
def send_git_diff(repository, diff_refs)
params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_send_git_diff, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT)
{
......
namespace :gitlab do
desc "GitLab | Setup production application"
task setup: :gitlab_environment do
check_gitaly_connection
setup_db
end
def check_gitaly_connection
Gitlab.config.repositories.storages.each do |name, _details|
Gitlab::GitalyClient::ServerService.new(name).info
end
rescue GRPC::Unavailable => ex
puts "Failed to connect to Gitaly...".color(:red)
puts "Error: #{ex}"
exit 1
end
def setup_db
warn_user_is_not_gitlab
......
......@@ -1776,6 +1776,9 @@ msgstr ""
msgid "Failed to change the owner"
msgstr ""
msgid "Failed to check related branches."
msgstr ""
msgid "Failed to remove issue from board, please try again."
msgstr ""
......
......@@ -115,8 +115,8 @@ from within the `qa` directory.
## Where to ask for help?
If you need more information, ask for help on `#qa` channel on Slack (GitLab
Team only).
If you need more information, ask for help on `#quality` channel on Slack
(internal, GitLab Team only).
If you are not a Team Member, and you still need help to contribute, please
open an issue in GitLab QA issue tracker.
......@@ -31,7 +31,7 @@ module QA
current changes in this merge request.
For more help see documentation in `qa/page/README.md` file or
ask for help on #qa channel on Slack (GitLab Team only).
ask for help on #quality channel on Slack (GitLab Team only).
If you are not a Team Member, and you still need help to
contribute, please open an issue in GitLab QA issue tracker.
......
......@@ -40,6 +40,30 @@ describe Projects::RepositoriesController do
expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-archive:")
end
it 'handles legacy queries with the ref specified as ref in params' do
get :archive, namespace_id: project.namespace, project_id: project, ref: 'feature', format: 'zip'
expect(response).to have_gitlab_http_status(200)
expect(assigns(:ref)).to eq('feature')
expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-archive:")
end
it 'handles legacy queries with the ref specified as id in params' do
get :archive, namespace_id: project.namespace, project_id: project, id: 'feature', format: 'zip'
expect(response).to have_gitlab_http_status(200)
expect(assigns(:ref)).to eq('feature')
expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-archive:")
end
it 'prioritizes the id param over the ref param when both are specified' do
get :archive, namespace_id: project.namespace, project_id: project, id: 'feature', ref: 'feature_conflict', format: 'zip'
expect(response).to have_gitlab_http_status(200)
expect(assigns(:ref)).to eq('feature')
expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-archive:")
end
context "when the service raises an error" do
before do
allow(Gitlab::Workhorse).to receive(:send_git_archive).and_raise("Archive failed")
......
......@@ -63,6 +63,13 @@ describe 'Issue Boards new issue', :js do
page.within(first('.board .issue-count-badge-count')) do
expect(page).to have_content('1')
end
page.within(first('.card')) do
issue = project.issues.find_by_title('bug')
expect(page).to have_content(issue.to_reference)
expect(page).to have_link(issue.title, href: issue_path(issue))
end
end
it 'shows sidebar when creating new issue' do
......
......@@ -14,7 +14,7 @@ feature 'Manually create a todo item from issue', :js do
it 'creates todo when clicking button' do
page.within '.issuable-sidebar' do
click_button 'Add todo'
expect(page).to have_content 'Mark done'
expect(page).to have_content 'Mark todo as done'
end
page.within '.header-content .todos-count' do
......@@ -31,7 +31,7 @@ feature 'Manually create a todo item from issue', :js do
it 'marks a todo as done' do
page.within '.issuable-sidebar' do
click_button 'Add todo'
click_button 'Mark done'
click_button 'Mark todo as done'
end
expect(page).to have_selector('.todos-count', visible: false)
......
......@@ -5,7 +5,7 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
let(:user) { project.creator }
let(:guest) { create(:user) }
let(:merge_request) { create(:merge_request_with_diffs, source_project: project, author: user, title: "Bug NS-04") }
let!(:note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request) }
let!(:note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, note: "| Markdown | Table |\n|-------|---------|\n| first | second |") }
let(:path) { "files/ruby/popen.rb" }
let(:position) do
Gitlab::Diff::Position.new(
......@@ -111,6 +111,15 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
expect(page.find(".line-holder-placeholder")).to be_visible
expect(page.find(".timeline-content #note_#{note.id}")).to be_visible
end
it 'renders tables in lazy-loaded resolved diff dicussions' do
find(".timeline-content .discussion[data-discussion-id='#{note.discussion_id}'] .discussion-toggle-button").click
wait_for_requests
expect(page.find(".timeline-content #note_#{note.id}")).not_to have_css(".line_holder")
expect(page.find(".timeline-content #note_#{note.id}")).to have_css("tr", count: 2)
end
end
describe 'side-by-side view' do
......
......@@ -22,11 +22,15 @@ describe IssuablesHelper do
end
describe '#issuable_labels_tooltip' do
it 'returns label text' do
it 'returns label text with no labels' do
expect(issuable_labels_tooltip([])).to eq("Labels")
end
it 'returns label text with labels within max limit' do
expect(issuable_labels_tooltip([label])).to eq(label.title)
end
it 'returns label text' do
it 'returns label text with labels exceeding max limit' do
expect(issuable_labels_tooltip([label, label2], limit: 1)).to eq("#{label.title}, and 1 more")
end
end
......
......@@ -83,58 +83,4 @@ describe MilestonesHelper do
end
end
end
describe '#milestone_remaining_days' do
around do |example|
Timecop.freeze(Time.utc(2017, 3, 17)) { example.run }
end
context 'when less than 31 days remaining' do
let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 12.days.from_now.utc)) }
it 'returns days remaining' do
expect(milestone_remaining).to eq("<strong>12</strong> days remaining")
end
end
context 'when less than 1 year and more than 30 days remaining' do
let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 2.months.from_now.utc)) }
it 'returns months remaining' do
expect(milestone_remaining).to eq("<strong>2</strong> months remaining")
end
end
context 'when more than 1 year remaining' do
let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: (1.year.from_now + 2.days).utc)) }
it 'returns years remaining' do
expect(milestone_remaining).to eq("<strong>1</strong> year remaining")
end
end
context 'when milestone is expired' do
let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 2.days.ago.utc)) }
it 'returns "Past due"' do
expect(milestone_remaining).to eq("<strong>Past due</strong>")
end
end
context 'when milestone has start_date in the future' do
let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, start_date: 2.days.from_now.utc)) }
it 'returns "Upcoming"' do
expect(milestone_remaining).to eq("<strong>Upcoming</strong>")
end
end
context 'when milestone has start_date in the past' do
let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, start_date: 2.days.ago.utc)) }
it 'returns days elapsed' do
expect(milestone_remaining).to eq("<strong>2</strong> days elapsed")
end
end
end
end
......@@ -85,7 +85,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => {
setTimeout(() => {
expect(
document.querySelector('.issuable-sidebar-header .js-issuable-todo').textContent.trim(),
).toBe('Mark done');
).toBe('Mark todo as done');
done();
});
......@@ -97,7 +97,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => {
setTimeout(() => {
expect(
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('data-original-title'),
).toBe('Mark done');
).toBe('Mark todo as done');
done();
});
......@@ -128,13 +128,13 @@ describe('Issuable right sidebar collapsed todo toggle', () => {
.catch(done.fail);
});
it('updates aria-label to mark done', (done) => {
it('updates aria-label to mark todo as done', (done) => {
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
setTimeout(() => {
expect(
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('aria-label'),
).toBe('Mark done');
).toBe('Mark todo as done');
done();
});
......@@ -147,7 +147,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => {
.then(() => {
expect(
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('aria-label'),
).toBe('Mark done');
).toBe('Mark todo as done');
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
})
......
......@@ -222,7 +222,7 @@ describe('RepoEditor', () => {
vm.setupEditor();
expect(vm.editor.onPositionChange).toHaveBeenCalled();
expect(vm.model.events.size).toBe(1);
expect(vm.model.events.size).toBe(2);
});
it('updates state when model content changed', done => {
......
......@@ -83,13 +83,6 @@ describe('Multi-file editor library model', () => {
});
describe('onChange', () => {
it('caches event by path', () => {
model.onChange(() => {});
expect(model.events.size).toBe(1);
expect(model.events.keys().next().value).toBe(model.file.key);
});
it('calls callback on change', done => {
const spy = jasmine.createSpy();
model.onChange(spy);
......@@ -132,5 +125,15 @@ describe('Multi-file editor library model', () => {
jasmine.anything(),
);
});
it('calls onDispose callback', () => {
const disposeSpy = jasmine.createSpy();
model.onDispose(disposeSpy);
model.dispose();
expect(disposeSpy).toHaveBeenCalled();
});
});
});
......@@ -117,4 +117,33 @@ describe('Multi-file editor library decorations controller', () => {
expect(controller.editorDecorations.size).toBe(0);
});
});
describe('hasDecorations', () => {
it('returns true when decorations are cached', () => {
controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
expect(controller.hasDecorations(model)).toBe(true);
});
it('returns false when no model decorations exist', () => {
expect(controller.hasDecorations(model)).toBe(false);
});
});
describe('removeDecorations', () => {
beforeEach(() => {
controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
controller.decorate(model);
});
it('removes cached decorations', () => {
expect(controller.decorations.size).not.toBe(0);
expect(controller.editorDecorations.size).not.toBe(0);
controller.removeDecorations(model);
expect(controller.decorations.size).toBe(0);
expect(controller.editorDecorations.size).toBe(0);
});
});
});
......@@ -3,10 +3,7 @@ import monacoLoader from '~/ide/monaco_loader';
import editor from '~/ide/lib/editor';
import ModelManager from '~/ide/lib/common/model_manager';
import DecorationsController from '~/ide/lib/decorations/controller';
import DirtyDiffController, {
getDiffChangeType,
getDecorator,
} from '~/ide/lib/diff/controller';
import DirtyDiffController, { getDiffChangeType, getDecorator } from '~/ide/lib/diff/controller';
import { computeDiff } from '~/ide/lib/diff/diff';
import { file } from '../../helpers';
......@@ -90,6 +87,14 @@ describe('Multi-file editor library dirty diff controller', () => {
expect(model.onChange).toHaveBeenCalled();
});
it('adds dispose event callback', () => {
spyOn(model, 'onDispose');
controller.attachModel(model);
expect(model.onDispose).toHaveBeenCalled();
});
it('calls throttledComputeDiff on change', () => {
spyOn(controller, 'throttledComputeDiff');
......@@ -99,6 +104,12 @@ describe('Multi-file editor library dirty diff controller', () => {
expect(controller.throttledComputeDiff).toHaveBeenCalled();
});
it('caches model', () => {
controller.attachModel(model);
expect(controller.models.has(model.url)).toBe(true);
});
});
describe('computeDiff', () => {
......@@ -116,14 +127,22 @@ describe('Multi-file editor library dirty diff controller', () => {
});
describe('reDecorate', () => {
it('calls decorations controller decorate', () => {
it('calls computeDiff when no decorations are cached', () => {
spyOn(controller, 'computeDiff');
controller.reDecorate(model);
expect(controller.computeDiff).toHaveBeenCalledWith(model);
});
it('calls decorate when decorations are cached', () => {
spyOn(controller.decorationsController, 'decorate');
controller.decorationsController.decorations.set(model.url, 'test');
controller.reDecorate(model);
expect(controller.decorationsController.decorate).toHaveBeenCalledWith(
model,
);
expect(controller.decorationsController.decorate).toHaveBeenCalledWith(model);
});
});
......@@ -133,16 +152,15 @@ describe('Multi-file editor library dirty diff controller', () => {
controller.decorate({ data: { changes: [], path: model.path } });
expect(
controller.decorationsController.addDecorations,
).toHaveBeenCalledWith(model, 'dirtyDiff', jasmine.anything());
expect(controller.decorationsController.addDecorations).toHaveBeenCalledWith(
model,
'dirtyDiff',
jasmine.anything(),
);
});
it('adds decorations into editor', () => {
const spy = spyOn(
controller.decorationsController.editor.instance,
'deltaDecorations',
);
const spy = spyOn(controller.decorationsController.editor.instance, 'deltaDecorations');
controller.decorate({
data: { changes: computeDiff('123', '1234'), path: model.path },
......@@ -181,16 +199,22 @@ describe('Multi-file editor library dirty diff controller', () => {
});
it('removes worker event listener', () => {
spyOn(
controller.dirtyDiffWorker,
'removeEventListener',
).and.callThrough();
spyOn(controller.dirtyDiffWorker, 'removeEventListener').and.callThrough();
controller.dispose();
expect(
controller.dirtyDiffWorker.removeEventListener,
).toHaveBeenCalledWith('message', jasmine.anything());
expect(controller.dirtyDiffWorker.removeEventListener).toHaveBeenCalledWith(
'message',
jasmine.anything(),
);
});
it('clears cached models', () => {
controller.attachModel(model);
model.dispose();
expect(controller.models.size).toBe(0);
});
});
});
......@@ -92,6 +92,7 @@ describe('Issue', function() {
function mockCanCreateBranch(canCreateBranch) {
mock.onGet(/(.*)\/can_create_branch$/).reply(200, {
can_create_branch: canCreateBranch,
suggested_branch_name: 'foo-99',
});
}
......
......@@ -156,4 +156,15 @@ describe Gitlab::GitalyClient::RepositoryService do
client.calculate_checksum
end
end
describe '#create_from_snapshot' do
it 'sends a create_repository_from_snapshot message' do
expect_any_instance_of(Gitaly::RepositoryService::Stub)
.to receive(:create_repository_from_snapshot)
.with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
.and_return(double)
client.create_from_snapshot('http://example.com?wiki=1', 'Custom xyz')
end
end
end
......@@ -537,12 +537,6 @@ ProjectCustomAttribute:
- project_id
- key
- value
LfsFileLock:
- id
- path
- user_id
- project_id
- created_at
Badge:
- id
- link_url
......
......@@ -482,4 +482,26 @@ describe Gitlab::Workhorse do
}.deep_stringify_keys)
end
end
describe '.send_git_snapshot' do
let(:url) { 'http://example.com' }
subject(:request) { described_class.send_git_snapshot(repository) }
it 'sets the header correctly' do
key, command, params = decode_workhorse_header(request)
expect(key).to eq("Gitlab-Workhorse-Send-Data")
expect(command).to eq('git-snapshot')
expect(params).to eq(
'GitalyServer' => {
'address' => Gitlab::GitalyClient.address(project.repository_storage),
'token' => Gitlab::GitalyClient.token(project.repository_storage)
},
'GetSnapshotRequest' => Gitaly::GetSnapshotRequest.new(
repository: repository.gitaly_repository
).to_json
)
end
end
end
......@@ -22,6 +22,15 @@ describe Uniquify do
expect(result).to eq('test_string2')
end
it 'allows to pass an initial value for the counter' do
start_counting_from = 2
uniquify = described_class.new(start_counting_from)
result = uniquify.string('test_string') { |s| s == 'test_string' }
expect(result).to eq('test_string2')
end
it 'allows passing in a base function that defines the location of the counter' do
result = uniquify.string(-> (counter) { "test_#{counter}_string" }) do |s|
s == 'test__string'
......
......@@ -376,6 +376,48 @@ describe Issue do
end
end
describe '#suggested_branch_name' do
let(:repository) { double }
subject { build(:issue) }
before do
allow(subject.project).to receive(:repository).and_return(repository)
end
context '#to_branch_name does not exists' do
before do
allow(repository).to receive(:branch_exists?).and_return(false)
end
it 'returns #to_branch_name' do
expect(subject.suggested_branch_name).to eq(subject.to_branch_name)
end
end
context '#to_branch_name exists not ending with -index' do
before do
allow(repository).to receive(:branch_exists?).and_return(true)
allow(repository).to receive(:branch_exists?).with(/#{subject.to_branch_name}-\d/).and_return(false)
end
it 'returns #to_branch_name ending with -2' do
expect(subject.suggested_branch_name).to eq("#{subject.to_branch_name}-2")
end
end
context '#to_branch_name exists ending with -index' do
before do
allow(repository).to receive(:branch_exists?).and_return(true)
allow(repository).to receive(:branch_exists?).with("#{subject.to_branch_name}-3").and_return(false)
end
it 'returns #to_branch_name ending with max index + 1' do
expect(subject.suggested_branch_name).to eq("#{subject.to_branch_name}-3")
end
end
end
describe '#has_related_branch?' do
let(:issue) { create(:issue, title: "Blue Bell Knoll") }
subject { issue.has_related_branch? }
......@@ -425,6 +467,27 @@ describe Issue do
end
end
describe '#can_be_worked_on?' do
let(:project) { build(:project) }
subject { build(:issue, :opened, project: project) }
context 'is closed' do
subject { build(:issue, :closed) }
it { is_expected.not_to be_can_be_worked_on }
end
context 'project is forked' do
before do
allow(project).to receive(:forked?).and_return(true)
end
it { is_expected.not_to be_can_be_worked_on }
end
it { is_expected.to be_can_be_worked_on }
end
describe '#participants' do
context 'using a public project' do
let(:project) { create(:project, :public) }
......
......@@ -96,7 +96,9 @@ describe Milestone do
allow(milestone).to receive(:due_date).and_return(Date.today.prev_year)
end
it { expect(milestone.expired?).to be_truthy }
it 'returns true when due_date is in the past' do
expect(milestone.expired?).to be_truthy
end
end
context "not expired" do
......@@ -104,17 +106,19 @@ describe Milestone do
allow(milestone).to receive(:due_date).and_return(Date.today.next_year)
end
it { expect(milestone.expired?).to be_falsey }
it 'returns false when due_date is in the future' do
expect(milestone.expired?).to be_falsey
end
end
end
describe '#upcoming?' do
it 'returns true' do
it 'returns true when start_date is in the future' do
milestone = build(:milestone, start_date: Time.now + 1.month)
expect(milestone.upcoming?).to be_truthy
end
it 'returns false' do
it 'returns false when start_date is in the past' do
milestone = build(:milestone, start_date: Date.today.prev_year)
expect(milestone.upcoming?).to be_falsey
end
......
......@@ -91,6 +91,23 @@ describe Note do
it "keeps the commit around" do
expect(note.project.repository.kept_around?(commit.id)).to be_truthy
end
it 'does not generate N+1 queries for participants', :request_store do
def retrieve_participants
commit.notes_with_associations.map(&:participants).to_a
end
# Project authorization checks are cached, establish a baseline
retrieve_participants
control_count = ActiveRecord::QueryRecorder.new do
retrieve_participants
end
create(:note_on_commit, project: note.project, note: 'another note', noteable_id: commit.id)
expect { retrieve_participants }.not_to exceed_query_limit(control_count)
end
end
describe 'authorization' do
......
require 'spec_helper'
describe API::ProjectSnapshots do
include WorkhorseHelpers
let(:project) { create(:project) }
let(:admin) { create(:admin) }
describe 'GET /projects/:id/snapshot' do
def expect_snapshot_response_for(repository)
type, params = workhorse_send_data
expect(type).to eq('git-snapshot')
expect(params).to eq(
'GitalyServer' => {
'address' => Gitlab::GitalyClient.address(repository.project.repository_storage),
'token' => Gitlab::GitalyClient.token(repository.project.repository_storage)
},
'GetSnapshotRequest' => Gitaly::GetSnapshotRequest.new(
repository: repository.gitaly_repository
).to_json
)
end
it 'returns authentication error as project owner' do
get api("/projects/#{project.id}/snapshot", project.owner)
expect(response).to have_gitlab_http_status(403)
end
it 'returns authentication error as unauthenticated user' do
get api("/projects/#{project.id}/snapshot", nil)
expect(response).to have_gitlab_http_status(401)
end
it 'requests project repository raw archive as administrator' do
get api("/projects/#{project.id}/snapshot", admin), wiki: '0'
expect(response).to have_gitlab_http_status(200)
expect_snapshot_response_for(project.repository)
end
it 'requests wiki repository raw archive as administrator' do
get api("/projects/#{project.id}/snapshot", admin), wiki: '1'
expect(response).to have_gitlab_http_status(200)
expect_snapshot_response_for(project.wiki.repository)
end
end
end
......@@ -32,6 +32,7 @@ describe EntityDateHelper do
end
it 'converts 86560 seconds' do
Rails.logger.debug date_helper_class.inspect
expect(date_helper_class.distance_of_time_as_hash(86560)).to eq(days: 1, mins: 2, seconds: 40)
end
......@@ -42,4 +43,58 @@ describe EntityDateHelper do
it 'converts 986760 seconds' do
expect(date_helper_class.distance_of_time_as_hash(986760)).to eq(days: 11, hours: 10, mins: 6)
end
describe '#remaining_days_in_words' do
around do |example|
Timecop.freeze(Time.utc(2017, 3, 17)) { example.run }
end
context 'when less than 31 days remaining' do
let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, due_date: 12.days.from_now.utc)) }
it 'returns days remaining' do
expect(milestone_remaining).to eq("<strong>12</strong> days remaining")
end
end
context 'when less than 1 year and more than 30 days remaining' do
let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, due_date: 2.months.from_now.utc)) }
it 'returns months remaining' do
expect(milestone_remaining).to eq("<strong>2</strong> months remaining")
end
end
context 'when more than 1 year remaining' do
let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, due_date: (1.year.from_now + 2.days).utc)) }
it 'returns years remaining' do
expect(milestone_remaining).to eq("<strong>1</strong> year remaining")
end
end
context 'when milestone is expired' do
let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, due_date: 2.days.ago.utc)) }
it 'returns "Past due"' do
expect(milestone_remaining).to eq("<strong>Past due</strong>")
end
end
context 'when milestone has start_date in the future' do
let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, start_date: 2.days.from_now.utc)) }
it 'returns "Upcoming"' do
expect(milestone_remaining).to eq("<strong>Upcoming</strong>")
end
end
context 'when milestone has start_date in the past' do
let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, start_date: 2.days.ago.utc)) }
it 'returns days elapsed' do
expect(milestone_remaining).to eq("<strong>2</strong> days elapsed")
end
end
end
end
......@@ -7,7 +7,7 @@ describe Projects::CreateFromTemplateService do
path: user.to_param,
template_name: 'rails',
description: 'project description',
visibility_level: Gitlab::VisibilityLevel::PRIVATE
visibility_level: Gitlab::VisibilityLevel::PUBLIC
}
end
......@@ -24,7 +24,23 @@ describe Projects::CreateFromTemplateService do
expect(project).to be_saved
expect(project.scheduled?).to be(true)
expect(project.description).to match('project description')
expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
end
context 'the result project' do
before do
Sidekiq::Testing.inline! do
@project = subject.execute
end
@project.reload
end
it 'overrides template description' do
expect(@project.description).to match('project description')
end
it 'overrides template visibility_level' do
expect(@project.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC)
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment