Commit 50c66197 authored by Stan Hu's avatar Stan Hu

Merge branch 'ce-to-ee-2018-03-01' into 'master'

CE upstream - 2018-03-01 16:38 UTC

Closes gitaly#1032 and gitlab-ce#43278

See merge request gitlab-org/gitlab-ee!4788
parents 3cfd6fa5 d66b7003
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.6-golang-1.9-git-2.14-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6" image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.6-golang-1.9-git-2.16-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6"
.dedicated-runner: &dedicated-runner .dedicated-runner: &dedicated-runner
retry: 1 retry: 1
......
...@@ -10,14 +10,22 @@ import DropdownUser from './dropdown_user'; ...@@ -10,14 +10,22 @@ import DropdownUser from './dropdown_user';
import FilteredSearchVisualTokens from './filtered_search_visual_tokens'; import FilteredSearchVisualTokens from './filtered_search_visual_tokens';
export default class FilteredSearchDropdownManager { export default class FilteredSearchDropdownManager {
constructor(baseEndpoint = '', tokenizer, page, isGroup, filteredSearchTokenKeys) { constructor({
baseEndpoint = '',
tokenizer,
page,
isGroup,
isGroupAncestor,
filteredSearchTokenKeys,
}) {
this.container = FilteredSearchContainer.container; this.container = FilteredSearchContainer.container;
this.baseEndpoint = baseEndpoint.replace(/\/$/, ''); this.baseEndpoint = baseEndpoint.replace(/\/$/, '');
this.tokenizer = tokenizer; this.tokenizer = tokenizer;
this.filteredSearchTokenKeys = filteredSearchTokenKeys || FilteredSearchTokenKeys; this.filteredSearchTokenKeys = filteredSearchTokenKeys || FilteredSearchTokenKeys;
this.filteredSearchInput = this.container.querySelector('.filtered-search'); this.filteredSearchInput = this.container.querySelector('.filtered-search');
this.page = page; this.page = page;
this.groupsOnly = page === 'boards' && isGroup; this.groupsOnly = isGroup;
this.groupAncestor = isGroupAncestor;
this.setupMapping(); this.setupMapping();
...@@ -60,7 +68,7 @@ export default class FilteredSearchDropdownManager { ...@@ -60,7 +68,7 @@ export default class FilteredSearchDropdownManager {
reference: null, reference: null,
gl: DropdownNonUser, gl: DropdownNonUser,
extraArguments: { extraArguments: {
endpoint: `${this.baseEndpoint}/milestones.json${this.groupsOnly ? '?only_group_milestones=true' : ''}`, endpoint: this.getMilestoneEndpoint(),
symbol: '%', symbol: '%',
}, },
element: this.container.querySelector('#js-dropdown-milestone'), element: this.container.querySelector('#js-dropdown-milestone'),
...@@ -69,7 +77,7 @@ export default class FilteredSearchDropdownManager { ...@@ -69,7 +77,7 @@ export default class FilteredSearchDropdownManager {
reference: null, reference: null,
gl: DropdownNonUser, gl: DropdownNonUser,
extraArguments: { extraArguments: {
endpoint: `${this.baseEndpoint}/labels.json${this.groupsOnly ? '?only_group_labels=true' : ''}`, endpoint: this.getLabelsEndpoint(),
symbol: '~', symbol: '~',
preprocessing: DropdownUtils.duplicateLabelPreprocessing, preprocessing: DropdownUtils.duplicateLabelPreprocessing,
}, },
...@@ -96,6 +104,28 @@ export default class FilteredSearchDropdownManager { ...@@ -96,6 +104,28 @@ export default class FilteredSearchDropdownManager {
this.mapping = allowedMappings; this.mapping = allowedMappings;
} }
getMilestoneEndpoint() {
let endpoint = `${this.baseEndpoint}/milestones.json`;
// EE-only
if (this.groupsOnly) {
endpoint = `${endpoint}?only_group_milestones=true`;
}
return endpoint;
}
getLabelsEndpoint() {
let endpoint = `${this.baseEndpoint}/labels.json`;
// EE-only
if (this.groupsOnly) {
endpoint = `${endpoint}?only_group_labels=true`;
}
return endpoint;
}
static addWordToInput(tokenName, tokenValue = '', clicked = false) { static addWordToInput(tokenName, tokenValue = '', clicked = false) {
const input = FilteredSearchContainer.container.querySelector('.filtered-search'); const input = FilteredSearchContainer.container.querySelector('.filtered-search');
......
...@@ -20,10 +20,13 @@ import DropdownUtils from './dropdown_utils'; ...@@ -20,10 +20,13 @@ import DropdownUtils from './dropdown_utils';
export default class FilteredSearchManager { export default class FilteredSearchManager {
constructor({ constructor({
page, page,
isGroup = false,
isGroupAncestor = false,
filteredSearchTokenKeys = FilteredSearchTokenKeys, filteredSearchTokenKeys = FilteredSearchTokenKeys,
stateFiltersSelector = '.issues-state-filters', stateFiltersSelector = '.issues-state-filters',
}) { }) {
this.isGroup = false; this.isGroup = isGroup;
this.isGroupAncestor = isGroupAncestor;
this.states = ['opened', 'closed', 'merged', 'all']; this.states = ['opened', 'closed', 'merged', 'all'];
this.page = page; this.page = page;
...@@ -98,13 +101,14 @@ export default class FilteredSearchManager { ...@@ -98,13 +101,14 @@ export default class FilteredSearchManager {
if (this.filteredSearchInput) { if (this.filteredSearchInput) {
this.tokenizer = FilteredSearchTokenizer; this.tokenizer = FilteredSearchTokenizer;
this.dropdownManager = new FilteredSearchDropdownManager( this.dropdownManager = new FilteredSearchDropdownManager({
this.filteredSearchInput.getAttribute('data-base-endpoint') || '', baseEndpoint: this.filteredSearchInput.getAttribute('data-base-endpoint') || '',
this.tokenizer, tokenizer: this.tokenizer,
this.page, page: this.page,
this.isGroup, isGroup: this.isGroup,
this.filteredSearchTokenKeys, isGroupAncestor: this.isGroupAncestor,
); filteredSearchTokenKeys: this.filteredSearchTokenKeys,
});
this.recentSearchesRoot = new RecentSearchesRoot( this.recentSearchesRoot = new RecentSearchesRoot(
this.recentSearchesStore, this.recentSearchesStore,
......
import FilteredSearchManager from '~/filtered_search/filtered_search_manager'; import FilteredSearchManager from '~/filtered_search/filtered_search_manager';
export default ({ page, filteredSearchTokenKeys, stateFiltersSelector }) => { export default ({
page,
filteredSearchTokenKeys,
isGroup,
isGroupAncestor,
stateFiltersSelector,
}) => {
const filteredSearchEnabled = FilteredSearchManager && document.querySelector('.filtered-search'); const filteredSearchEnabled = FilteredSearchManager && document.querySelector('.filtered-search');
if (filteredSearchEnabled) { if (filteredSearchEnabled) {
const filteredSearchManager = new FilteredSearchManager({ const filteredSearchManager = new FilteredSearchManager({
page, page,
isGroup,
isGroupAncestor,
filteredSearchTokenKeys, filteredSearchTokenKeys,
stateFiltersSelector, stateFiltersSelector,
}); });
......
...@@ -316,7 +316,7 @@ ...@@ -316,7 +316,7 @@
v-if="pipeline.flags.cancelable" v-if="pipeline.flags.cancelable"
:endpoint="pipeline.cancel_path" :endpoint="pipeline.cancel_path"
css-class="js-pipelines-cancel-button btn-remove" css-class="js-pipelines-cancel-button btn-remove"
title="Cancel" title="Stop"
icon="close" icon="close"
:pipeline-id="pipeline.id" :pipeline-id="pipeline.id"
data-toggle="modal" data-toggle="modal"
......
...@@ -14,6 +14,11 @@ ...@@ -14,6 +14,11 @@
collapsedCalendarIcon, collapsedCalendarIcon,
}, },
props: { props: {
blockClass: {
type: String,
required: false,
default: '',
},
collapsed: { collapsed: {
type: Boolean, type: Boolean,
required: false, required: false,
...@@ -91,7 +96,10 @@ ...@@ -91,7 +96,10 @@
</script> </script>
<template> <template>
<div class="block"> <div
class="block"
:class="blockClass"
>
<div class="issuable-sidebar-header"> <div class="issuable-sidebar-header">
<toggle-sidebar <toggle-sidebar
:collapsed="collapsed" :collapsed="collapsed"
......
class Profiles::PasswordsController < Profiles::ApplicationController class Profiles::PasswordsController < Profiles::ApplicationController
skip_before_action :check_password_expiration, only: [:new, :create] skip_before_action :check_password_expiration, only: [:new, :create]
skip_before_action :check_two_factor_requirement, only: [:new, :create]
before_action :set_user before_action :set_user
before_action :authorize_change_password! before_action :authorize_change_password!
......
...@@ -146,7 +146,7 @@ class Repository ...@@ -146,7 +146,7 @@ class Repository
end end
end end
def commits(ref, path: nil, limit: nil, offset: nil, skip_merges: false, after: nil, before: nil) def commits(ref = nil, path: nil, limit: nil, offset: nil, skip_merges: false, after: nil, before: nil, all: nil)
options = { options = {
repo: raw_repository, repo: raw_repository,
ref: ref, ref: ref,
...@@ -156,7 +156,8 @@ class Repository ...@@ -156,7 +156,8 @@ class Repository
after: after, after: after,
before: before, before: before,
follow: Array(path).length == 1, follow: Array(path).length == 1,
skip_merges: skip_merges skip_merges: skip_merges,
all: all
} }
commits = Gitlab::Git::Commit.where(options) commits = Gitlab::Git::Commit.where(options)
......
---
title: Allow commits endpoint to work over all commits of a repository
merge_request: 17182
author:
type: added
---
title: Update tooltip on pipeline cancel to Stop (#42946)
merge_request: 17444
author:
type: fixed
---
title: Add NOT NULL constraint to projects.namespace_id
merge_request: 17448
author:
type: other
---
title: Removing the two factor check when the user sets a new password
merge_request: 17457
author:
type: fixed
---
title: Encode branch name as binary before creating a RPC request to copy attributes
merge_request: 17291
author:
type: fixed
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class ChangeProjectNamespaceIdNotNull < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
class Project < ActiveRecord::Base
self.table_name = 'projects'
include EachBatch
end
BATCH_SIZE = 1000
DOWNTIME = false
disable_ddl_transaction!
def up
Project.where(namespace_id: nil).each_batch(of: BATCH_SIZE) do |batch|
batch.delete_all
end
change_column_null :projects, :namespace_id, false
end
def down
change_column_null :projects, :namespace_id, true
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20180222043024) do ActiveRecord::Schema.define(version: 20180301084653) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -1866,7 +1866,7 @@ ActiveRecord::Schema.define(version: 20180222043024) do ...@@ -1866,7 +1866,7 @@ ActiveRecord::Schema.define(version: 20180222043024) do
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.integer "creator_id" t.integer "creator_id"
t.integer "namespace_id" t.integer "namespace_id", null: false
t.datetime "last_activity_at" t.datetime "last_activity_at"
t.string "import_url" t.string "import_url"
t.integer "visibility_level", default: 0, null: false t.integer "visibility_level", default: 0, null: false
......
...@@ -14,6 +14,9 @@ GET /projects/:id/repository/commits ...@@ -14,6 +14,9 @@ GET /projects/:id/repository/commits
| `ref_name` | string | no | The name of a repository branch or tag or if not given the default branch | | `ref_name` | string | no | The name of a repository branch or tag or if not given the default branch |
| `since` | string | no | Only commits after or on this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ | | `since` | string | no | Only commits after or on this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ |
| `until` | string | no | Only commits before or on this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ | | `until` | string | no | Only commits before or on this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ |
| `path` | string | no | The file path |
| `all` | boolean | no | Retrieve every commit from the repository |
```bash ```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/repository/commits" curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/repository/commits"
......
...@@ -1204,6 +1204,7 @@ GET /projects/:id/hooks/:hook_id ...@@ -1204,6 +1204,7 @@ GET /projects/:id/hooks/:hook_id
"project_id": 3, "project_id": 3,
"push_events": true, "push_events": true,
"issues_events": true, "issues_events": true,
"confidential_issues_events": true,
"merge_requests_events": true, "merge_requests_events": true,
"tag_push_events": true, "tag_push_events": true,
"note_events": true, "note_events": true,
...@@ -1229,6 +1230,7 @@ POST /projects/:id/hooks ...@@ -1229,6 +1230,7 @@ POST /projects/:id/hooks
| `url` | string | yes | The hook URL | | `url` | string | yes | The hook URL |
| `push_events` | boolean | no | Trigger hook on push events | | `push_events` | boolean | no | Trigger hook on push events |
| `issues_events` | boolean | no | Trigger hook on issues events | | `issues_events` | boolean | no | Trigger hook on issues events |
| `confidential_issues_events` | boolean | no | Trigger hook on confidential issues events |
| `merge_requests_events` | boolean | no | Trigger hook on merge requests events | | `merge_requests_events` | boolean | no | Trigger hook on merge requests events |
| `tag_push_events` | boolean | no | Trigger hook on tag push events | | `tag_push_events` | boolean | no | Trigger hook on tag push events |
| `note_events` | boolean | no | Trigger hook on note events | | `note_events` | boolean | no | Trigger hook on note events |
...@@ -1253,6 +1255,7 @@ PUT /projects/:id/hooks/:hook_id ...@@ -1253,6 +1255,7 @@ PUT /projects/:id/hooks/:hook_id
| `url` | string | yes | The hook URL | | `url` | string | yes | The hook URL |
| `push_events` | boolean | no | Trigger hook on push events | | `push_events` | boolean | no | Trigger hook on push events |
| `issues_events` | boolean | no | Trigger hook on issues events | | `issues_events` | boolean | no | Trigger hook on issues events |
| `confidential_issues_events` | boolean | no | Trigger hook on confidential issues events |
| `merge_requests_events` | boolean | no | Trigger hook on merge requests events | | `merge_requests_events` | boolean | no | Trigger hook on merge requests events |
| `tag_push_events` | boolean | no | Trigger hook on tag push events | | `tag_push_events` | boolean | no | Trigger hook on tag push events |
| `note_events` | boolean | no | Trigger hook on note events | | `note_events` | boolean | no | Trigger hook on note events |
......
...@@ -619,6 +619,7 @@ Example response: ...@@ -619,6 +619,7 @@ Example response:
"active": true, "active": true,
"push_events": true, "push_events": true,
"issues_events": true, "issues_events": true,
"confidential_issues_events": true,
"merge_requests_events": true, "merge_requests_events": true,
"tag_push_events": true, "tag_push_events": true,
"note_events": true, "note_events": true,
......
...@@ -33,6 +33,40 @@ rest of the code should be as close to the CE files as possible. ...@@ -33,6 +33,40 @@ rest of the code should be as close to the CE files as possible.
[single code base]: https://gitlab.com/gitlab-org/gitlab-ee/issues/2952#note_41016454 [single code base]: https://gitlab.com/gitlab-org/gitlab-ee/issues/2952#note_41016454
### Detection of EE-only files
For each commit (except on `master`), the `ee-files-location-check` CI job tries
to detect if there are any new files that are EE-only. If any file is detected,
the job fails with an explanation of why and what to do to make it pass.
Basically, the fix is simple: `git mv <file> ee/<file>`.
#### How to name your branches?
For any EE branch, the job will try to detect its CE counterpart by removing any
`ee-` prefix or `-ee` suffix from the EE branch name, and matching the last
branch that contains it.
For instance, from the EE branch `new-shiny-feature-ee` (or
`ee-new-shiny-feature`), the job would find the corresponding CE branches:
- `new-shiny-feature`
- `ce-new-shiny-feature`
- `new-shiny-feature-ce`
- `my-super-new-shiny-feature-in-ce`
#### Whitelist some EE-only files that cannot be moved to `ee/`
The `ee-files-location-check` CI job provides a whitelist of files or folders
that cannot or should not be moved to `ee/`. Feel free to open an issue to
discuss adding a new file/folder to this whitelist.
For instance, it was decided that moving EE-only files from `qa/` to `ee/qa/`
would make it difficult to build the `gitLab-{ce,ee}-qa` Docker images and it
was [not worth the complexity].
[not worth the complexity]: https://gitlab.com/gitlab-org/gitlab-ee/issues/4997#note_59764702
### EE-only features ### EE-only features
If the feature being developed is not present in any form in CE, we don't If the feature being developed is not present in any form in CE, we don't
...@@ -52,6 +86,11 @@ is applied not only to models. Here's a list of other examples: ...@@ -52,6 +86,11 @@ is applied not only to models. Here's a list of other examples:
- `ee/app/validators/foo_attr_validator.rb` - `ee/app/validators/foo_attr_validator.rb`
- `ee/app/workers/foo_worker.rb` - `ee/app/workers/foo_worker.rb`
This works because for every path that are present in CE's eager-load/auto-load
paths, we add the same `ee/`-prepended path in [`config/application.rb`].
[`config/application.rb`]: https://gitlab.com/gitlab-org/gitlab-ee/blob/d278b76d6600a0e27d8019a0be27971ba23ab640/config/application.rb#L41-51
### EE features based on CE features ### EE features based on CE features
For features that build on existing CE features, write a module in the For features that build on existing CE features, write a module in the
......
...@@ -36,6 +36,15 @@ If you are asynchronously adding content which contains lazy images then you nee ...@@ -36,6 +36,15 @@ If you are asynchronously adding content which contains lazy images then you nee
`gl.lazyLoader.searchLazyImages()` which will search for lazy images and load them if needed. `gl.lazyLoader.searchLazyImages()` which will search for lazy images and load them if needed.
But in general it should be handled automatically through a `MutationObserver` in the lazy loading function. But in general it should be handled automatically through a `MutationObserver` in the lazy loading function.
### Animations
Only animate `opacity` & `transform` properties. Other properties (such as `top`, `left`, `margin`, and `padding`) all cause
Layout to be recalculated, which is much more expensive. For details on this, see "Styles that Affect Layout" in
[High Performance Animations][high-perf-animations].
If you _do_ need to change layout (e.g. a sidebar that pushes main content over), prefer [FLIP][flip] to change expensive
properties once, and handle the actual animation with transforms.
## Reducing Asset Footprint ## Reducing Asset Footprint
### Page-specific JavaScript ### Page-specific JavaScript
...@@ -87,6 +96,7 @@ General tips: ...@@ -87,6 +96,7 @@ General tips:
- Compress and minify assets wherever possible (For CSS/JS, Sprockets and webpack do this for us). - Compress and minify assets wherever possible (For CSS/JS, Sprockets and webpack do this for us).
- If some functionality can reasonably be achieved without adding extra libraries, avoid them. - If some functionality can reasonably be achieved without adding extra libraries, avoid them.
- Use page-specific JavaScript as described above to dynamically load libraries that are only needed on certain pages. - Use page-specific JavaScript as described above to dynamically load libraries that are only needed on certain pages.
- [High Performance Animations][high-perf-animations]
------- -------
...@@ -105,3 +115,5 @@ General tips: ...@@ -105,3 +115,5 @@ General tips:
[d3]: https://d3js.org/ [d3]: https://d3js.org/
[chartjs]: http://www.chartjs.org/ [chartjs]: http://www.chartjs.org/
[page-specific-js-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/13bb9ed77f405c5f6ee4fdbc964ecf635c9a223f/app/views/projects/graphs/_head.html.haml#L6-8 [page-specific-js-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/13bb9ed77f405c5f6ee4fdbc964ecf635c9a223f/app/views/projects/graphs/_head.html.haml#L6-8
[high-perf-animations]: https://www.html5rocks.com/en/tutorials/speed/high-performance-animations/
[flip]: https://aerotwist.com/blog/flip-your-animations/
...@@ -107,104 +107,28 @@ You can mark that content for translation with: ...@@ -107,104 +107,28 @@ You can mark that content for translation with:
### JavaScript files ### JavaScript files
In JavaScript we added the `__()` (double underscore parenthesis) function In JavaScript we added the `__()` (double underscore parenthesis) function that
for translations. you can import from the `~/locale` file. For instance:
In order to test JavaScript translations you have to change the GitLab localization to other language than English and you have to generate JSON files using `bundle exec rake gettext:po_to_json` or `bundle exec rake gettext:compile`. ```js
import { __ } from '~/locale';
## Updating the PO files with the new content const label = __('Subscribe');
Now that the new content is marked for translation, we need to update the PO
files with the following command:
```sh
bundle exec rake gettext:find
```
This command will update the `locale/gitlab.pot` file with the newly externalized
strings and remove any strings that aren't used anymore. You should check this
file in. Once the changes are on master, they will be picked up by
[Crowdin](http://translate.gitlab.com) and be presented for translation.
If there are merge conflicts in the `gitlab.pot` file, you can delete the file
and regenerate it using the same command. Confirm that you are not deleting any strings accidentally by looking over the diff.
The command also updates the translation files for each language: `locale/*/gitlab.po`
These changes can be discarded, the languange files will be updated by Crowdin
automatically.
Discard all of them at once like this:
```sh
git checkout locale/*/gitlab.po
```
### Validating PO files
To make sure we keep our translation files up to date, there's a linter that is
running on CI as part of the `static-analysis` job.
To lint the adjustments in PO files locally you can run `rake gettext:lint`.
The linter will take the following into account:
- Valid PO-file syntax
- Variable usage
- Only one unnamed (`%d`) variable, since the order of variables might change
in different languages
- All variables used in the message-id are used in the translation
- There should be no variables used in a translation that aren't in the
message-id
- Errors during translation.
The errors are grouped per file, and per message ID:
```
Errors in `locale/zh_HK/gitlab.po`:
PO-syntax errors
SimplePoParser::ParserErrorSyntax error in lines
Syntax error in msgctxt
Syntax error in msgid
Syntax error in msgstr
Syntax error in message_line
There should be only whitespace until the end of line after the double quote character of a message text.
Parseing result before error: '{:msgid=>["", "You are going to remove %{project_name_with_namespace}.\\n", "Removed project CANNOT be restored!\\n", "Are you ABSOLUTELY sure?"]}'
SimplePoParser filtered backtrace: SimplePoParser::ParserError
Errors in `locale/zh_TW/gitlab.po`:
1 pipeline
<%d 條流水線> is using unknown variables: [%d]
Failure translating to zh_TW with []: too few arguments
``` ```
In this output the `locale/zh_HK/gitlab.po` has syntax errors. In order to test JavaScript translations you have to change the GitLab
The `locale/zh_TW/gitlab.po` has variables that are used in the translation that localization to other language than English and you have to generate JSON files
aren't in the message with id `1 pipeline`. using `bin/rake gettext:po_to_json` or `bin/rake gettext:compile`.
## Working with special content
### Just marking content for parsing
- In Ruby/HAML:
```ruby
_('Subscribe')
```
- In JavaScript:
```js
import { __ } from '../../../locale';
const label = __('Subscribe');
```
### Dynamic translations
Sometimes there are some dynamic translations that can't be found by the Sometimes there are some dynamic translations that can't be found by the
parser when running `bundle exec rake gettext:find`. For these scenarios you can parser when running `bin/rake gettext:find`. For these scenarios you can
use the [`_N` method](https://github.com/grosser/gettext_i18n_rails/blob/c09e38d481e0899ca7d3fc01786834fa8e7aab97/Readme.md#unfound-translations-with-rake-gettextfind). use the [`N_` method](https://github.com/grosser/gettext_i18n_rails/blob/c09e38d481e0899ca7d3fc01786834fa8e7aab97/Readme.md#unfound-translations-with-rake-gettextfind).
There is also and alternative method to [translate messages from validation errors](https://github.com/grosser/gettext_i18n_rails/blob/c09e38d481e0899ca7d3fc01786834fa8e7aab97/Readme.md#option-a). There is also and alternative method to [translate messages from validation errors](https://github.com/grosser/gettext_i18n_rails/blob/c09e38d481e0899ca7d3fc01786834fa8e7aab97/Readme.md#option-a).
## Working with special content
### Interpolation ### Interpolation
- In Ruby/HAML: - In Ruby/HAML:
...@@ -216,7 +140,7 @@ There is also and alternative method to [translate messages from validation erro ...@@ -216,7 +140,7 @@ There is also and alternative method to [translate messages from validation erro
- In JavaScript: - In JavaScript:
```js ```js
import { __, sprintf } from '../../../locale'; import { __, sprintf } from '~/locale';
sprintf(__('Hello %{username}'), { username: 'Joe' }) => 'Hello Joe' sprintf(__('Hello %{username}'), { username: 'Joe' }) => 'Hello Joe'
``` ```
...@@ -228,24 +152,30 @@ For example use `%{created_at}` in Ruby but `%{createdAt}` in JavaScript. ...@@ -228,24 +152,30 @@ For example use `%{created_at}` in Ruby but `%{createdAt}` in JavaScript.
- In Ruby/HAML: - In Ruby/HAML:
```ruby ```ruby
n_('Apple', 'Apples', 3) => 'Apples' n_('Apple', 'Apples', 3)
# => 'Apples'
``` ```
Using interpolation: Using interpolation:
```ruby ```ruby
n_("There is a mouse.", "There are %d mice.", size) % size n_("There is a mouse.", "There are %d mice.", size) % size
# => When size == 1: 'There is a mouse.'
# => When size == 2: 'There are 2 mice.'
``` ```
- In JavaScript: - In JavaScript:
```js ```js
n__('Apple', 'Apples', 3) => 'Apples' n__('Apple', 'Apples', 3)
// => 'Apples'
``` ```
Using interpolation: Using interpolation:
```js ```js
n__('Last day', 'Last %d days', 30) => 'Last 30 days' n__('Last day', 'Last %d days', x)
// => When x == 1: 'Last day'
// => When x == 2: 'Last 2 days'
``` ```
### Namespaces ### Namespaces
...@@ -267,12 +197,15 @@ Sometimes you need to add some context to the text that you want to translate ...@@ -267,12 +197,15 @@ Sometimes you need to add some context to the text that you want to translate
s__('OpenedNDaysAgo|Opened') s__('OpenedNDaysAgo|Opened')
``` ```
Note: The namespace should be removed from the translation. See the [translation
guidelines for more details](./translation.md#namespaced-strings).
### Dates / times ### Dates / times
- In JavaScript: - In JavaScript:
```js ```js
import { createDateTimeFormat } from '.../locale'; import { createDateTimeFormat } from '~/locale';
const dateFormat = createDateTimeFormat({ year: 'numeric', month: 'long', day: 'numeric' }); const dateFormat = createDateTimeFormat({ year: 'numeric', month: 'long', day: 'numeric' });
console.log(dateFormat.format(new Date('2063-04-05'))) // April 5, 2063 console.log(dateFormat.format(new Date('2063-04-05'))) // April 5, 2063
...@@ -282,6 +215,100 @@ This makes use of [`Intl.DateTimeFormat`]. ...@@ -282,6 +215,100 @@ This makes use of [`Intl.DateTimeFormat`].
[`Intl.DateTimeFormat`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat [`Intl.DateTimeFormat`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat
## Best practices
### Splitting sentences
Please never split a sentence as that would assume the sentence grammar and
structure is the same in all languages.
For instance, the following
```js
{{ s__("mrWidget|Set by") }}
{{ author.name }}
{{ s__("mrWidget|to be merged automatically when the pipeline succeeds") }}
```
should be externalized as follows:
```js
{{ sprintf(s__("mrWidget|Set by %{author} to be merged automatically when the pipeline succeeds"), { author: author.name }) }}
```
When in doubt, try to follow the best practices described in this [Mozilla
Developer documentation][mdn].
[mdn]: https://developer.mozilla.org/en-US/docs/Mozilla/Localization/Localization_content_best_practices#Splitting
## Updating the PO files with the new content
Now that the new content is marked for translation, we need to update the PO
files with the following command:
```sh
bin/rake gettext:find
```
This command will update the `locale/gitlab.pot` file with the newly externalized
strings and remove any strings that aren't used anymore. You should check this
file in. Once the changes are on master, they will be picked up by
[Crowdin](http://translate.gitlab.com) and be presented for translation.
If there are merge conflicts in the `gitlab.pot` file, you can delete the file
and regenerate it using the same command. Confirm that you are not deleting any strings accidentally by looking over the diff.
The command also updates the translation files for each language: `locale/*/gitlab.po`
These changes can be discarded, the languange files will be updated by Crowdin
automatically.
Discard all of them at once like this:
```sh
git checkout locale/*/gitlab.po
```
### Validating PO files
To make sure we keep our translation files up to date, there's a linter that is
running on CI as part of the `static-analysis` job.
To lint the adjustments in PO files locally you can run `rake gettext:lint`.
The linter will take the following into account:
- Valid PO-file syntax
- Variable usage
- Only one unnamed (`%d`) variable, since the order of variables might change
in different languages
- All variables used in the message-id are used in the translation
- There should be no variables used in a translation that aren't in the
message-id
- Errors during translation.
The errors are grouped per file, and per message ID:
```
Errors in `locale/zh_HK/gitlab.po`:
PO-syntax errors
SimplePoParser::ParserErrorSyntax error in lines
Syntax error in msgctxt
Syntax error in msgid
Syntax error in msgstr
Syntax error in message_line
There should be only whitespace until the end of line after the double quote character of a message text.
Parseing result before error: '{:msgid=>["", "You are going to remove %{project_name_with_namespace}.\\n", "Removed project CANNOT be restored!\\n", "Are you ABSOLUTELY sure?"]}'
SimplePoParser filtered backtrace: SimplePoParser::ParserError
Errors in `locale/zh_TW/gitlab.po`:
1 pipeline
<%d 條流水線> is using unknown variables: [%d]
Failure translating to zh_TW with []: too few arguments
```
In this output the `locale/zh_HK/gitlab.po` has syntax errors.
The `locale/zh_TW/gitlab.po` has variables that are used in the translation that
aren't in the message with id `1 pipeline`.
## Adding a new language ## Adding a new language
Let's suppose you want to add translations for a new language, let's say French. Let's suppose you want to add translations for a new language, let's say French.
...@@ -300,14 +327,14 @@ Let's suppose you want to add translations for a new language, let's say French. ...@@ -300,14 +327,14 @@ Let's suppose you want to add translations for a new language, let's say French.
1. Next, you need to add the language: 1. Next, you need to add the language:
```sh ```sh
bundle exec rake gettext:add_language[fr] bin/rake gettext:add_language[fr]
``` ```
If you want to add a new language for a specific region, the command is similar, If you want to add a new language for a specific region, the command is similar,
you just need to separate the region with an underscore (`_`). For example: you just need to separate the region with an underscore (`_`). For example:
```sh ```sh
bundle exec rake gettext:add_language[en_GB] bin/rake gettext:add_language[en_GB]
``` ```
Please note that you need to specify the region part in capitals. Please note that you need to specify the region part in capitals.
...@@ -321,7 +348,7 @@ Let's suppose you want to add translations for a new language, let's say French. ...@@ -321,7 +348,7 @@ Let's suppose you want to add translations for a new language, let's say French.
containing the translations: containing the translations:
```sh ```sh
bundle exec rake gettext:compile bin/rake gettext:compile
``` ```
1. In order to see the translated content we need to change our preferred language 1. In order to see the translated content we need to change our preferred language
......
...@@ -15,6 +15,7 @@ are very appreciative of the work done by translators and proofreaders! ...@@ -15,6 +15,7 @@ are very appreciative of the work done by translators and proofreaders!
- Dutch - Dutch
- Esperanto - Esperanto
- French - French
- Rémy Coutable - [GitLab](https://gitlab.com/rymai), [Crowdin](https://crowdin.com/profile/rymai)
- German - German
- Italian - Italian
- Paolo Falomo - [GitLab](https://gitlab.com/paolofalomo), [Crowdin](https://crowdin.com/profile/paolo.falomo) - Paolo Falomo - [GitLab](https://gitlab.com/paolofalomo), [Crowdin](https://crowdin.com/profile/paolo.falomo)
......
...@@ -37,33 +37,43 @@ Comments can be added to discuss a translation with the community. ...@@ -37,33 +37,43 @@ Comments can be added to discuss a translation with the community.
Remember to **Save** each translation. Remember to **Save** each translation.
## Translation Guidelines ## General Translation Guidelines
Be sure to check the following guidelines before you translate any strings. Be sure to check the following guidelines before you translate any strings.
### Namespaced strings
When an externalized string is prepended with a namespace, e.g.
`s_('OpenedNDaysAgo|Opened')`, the namespace should be removed from the final
translation.
For example in French `OpenedNDaysAgo|Opened` would be translated to
`Ouvert•e`, not `OpenedNDaysAgo|Ouvert•e`.
### Technical terms ### Technical terms
Technical terms should be treated like proper nouns and not be translated. Some technical terms should be treated like proper nouns and not be translated.
This helps maintain a logical connection and consistency between tools (e.g. `git` client) and
GitLab.
Technical terms that should always be in English are noted in the glossary when using Technical terms that should always be in English are noted in the glossary when
[translate.gitlab.com](https://translate.gitlab.com). using [translate.gitlab.com](https://translate.gitlab.com).
This helps maintain a logical connection and consistency between tools (e.g.
`git` client) and GitLab.
### Formality ### Formality
The level of formality used in software varies by language. The level of formality used in software varies by language.
For example, in French we translate `you` as the informal `tu`. For example, in French we translate `you` as the formal `vous`.
You can refer to other translated strings and notes in the glossary to assist determining a You can refer to other translated strings and notes in the glossary to assist
suitable level of formality. determining a suitable level of formality.
### Inclusive language ### Inclusive language
[Diversity] is one of GitLab's values. [Diversity] is one of GitLab's values.
We ask you to avoid translations which exclude people based on their gender or ethnicity. We ask you to avoid translations which exclude people based on their gender or
In languages which distinguish between a male and female form, ethnicity.
use both or choose a neutral formulation. In languages which distinguish between a male and female form, use both or
choose a neutral formulation.
For example in German, the word "user" can be translated into "Benutzer" (male) or "Benutzerin" (female). For example in German, the word "user" can be translated into "Benutzer" (male) or "Benutzerin" (female).
Therefore "create a new user" would translate into "Benutzer(in) anlegen". Therefore "create a new user" would translate into "Benutzer(in) anlegen".
...@@ -74,3 +84,14 @@ Therefore "create a new user" would translate into "Benutzer(in) anlegen". ...@@ -74,3 +84,14 @@ Therefore "create a new user" would translate into "Benutzer(in) anlegen".
To propose additions to the glossary please To propose additions to the glossary please
[open an issue](https://gitlab.com/gitlab-org/gitlab-ce/issues). [open an issue](https://gitlab.com/gitlab-org/gitlab-ce/issues).
## French Translation Guidelines
### Inclusive language in French
In French, we should follow the guidelines from [ecriture-inclusive.fr]. For
instance:
- Utilisateur•rice•s
[ecriture-inclusive.fr]: http://www.ecriture-inclusive.fr/
...@@ -93,9 +93,9 @@ Is the system packaged Git too old? Remove it and compile from source. ...@@ -93,9 +93,9 @@ Is the system packaged Git too old? Remove it and compile from source.
# Download and compile from source # Download and compile from source
cd /tmp cd /tmp
curl --remote-name --progress https://www.kernel.org/pub/software/scm/git/git-2.14.3.tar.gz curl --remote-name --progress https://www.kernel.org/pub/software/scm/git/git-2.16.2.tar.gz
echo '023ffff6d3ba8a1bea779dfecc0ed0bb4ad68ab8601d14435dd8c08416f78d7f git-2.14.3.tar.gz' | shasum -a256 -c - && tar -xzf git-2.14.3.tar.gz echo '9acc4339b7a2ab484eea69d705923271682b7058015219cf5a7e6ed8dee5b5fb git-2.16.2.tar.gz' | shasum -a256 -c - && tar -xzf git-2.16.2.tar.gz
cd git-2.14.3/ cd git-2.16.2/
./configure ./configure
make prefix=/usr/local all make prefix=/usr/local all
......
...@@ -18,25 +18,28 @@ module API ...@@ -18,25 +18,28 @@ module API
optional :since, type: DateTime, desc: 'Only commits after or on this date will be returned' optional :since, type: DateTime, desc: 'Only commits after or on this date will be returned'
optional :until, type: DateTime, desc: 'Only commits before or on this date will be returned' optional :until, type: DateTime, desc: 'Only commits before or on this date will be returned'
optional :path, type: String, desc: 'The file path' optional :path, type: String, desc: 'The file path'
optional :all, type: Boolean, desc: 'Every commit will be returned'
use :pagination use :pagination
end end
get ':id/repository/commits' do get ':id/repository/commits' do
path = params[:path] path = params[:path]
before = params[:until] before = params[:until]
after = params[:since] after = params[:since]
ref = params[:ref_name] || user_project.try(:default_branch) || 'master' ref = params[:ref_name] || user_project.try(:default_branch) || 'master' unless params[:all]
offset = (params[:page] - 1) * params[:per_page] offset = (params[:page] - 1) * params[:per_page]
all = params[:all]
commits = user_project.repository.commits(ref, commits = user_project.repository.commits(ref,
path: path, path: path,
limit: params[:per_page], limit: params[:per_page],
offset: offset, offset: offset,
before: before, before: before,
after: after) after: after,
all: all)
commit_count = commit_count =
if path || before || after if all || path || before || after
user_project.repository.count_commits(ref: ref, path: path, before: before, after: after) user_project.repository.count_commits(ref: ref, path: path, before: before, after: after, all: all)
else else
# Cacheable commit count. # Cacheable commit count.
user_project.repository.commit_count_for_ref(ref) user_project.repository.commit_count_for_ref(ref)
......
...@@ -71,7 +71,7 @@ module API ...@@ -71,7 +71,7 @@ module API
end end
class ProjectHook < Hook class ProjectHook < Hook
expose :project_id, :issues_events expose :project_id, :issues_events, :confidential_issues_events
expose :note_events, :pipeline_events, :wiki_page_events expose :note_events, :pipeline_events, :wiki_page_events
expose :job_events expose :job_events
end end
......
...@@ -10,6 +10,7 @@ module API ...@@ -10,6 +10,7 @@ module API
requires :url, type: String, desc: "The URL to send the request to" requires :url, type: String, desc: "The URL to send the request to"
optional :push_events, type: Boolean, desc: "Trigger hook on push events" optional :push_events, type: Boolean, desc: "Trigger hook on push events"
optional :issues_events, type: Boolean, desc: "Trigger hook on issues events" optional :issues_events, type: Boolean, desc: "Trigger hook on issues events"
optional :confidential_issues_events, type: Boolean, desc: "Trigger hook on confidential issues events"
optional :merge_requests_events, type: Boolean, desc: "Trigger hook on merge request events" optional :merge_requests_events, type: Boolean, desc: "Trigger hook on merge request events"
optional :tag_push_events, type: Boolean, desc: "Trigger hook on tag push events" optional :tag_push_events, type: Boolean, desc: "Trigger hook on tag push events"
optional :note_events, type: Boolean, desc: "Trigger hook on note(comment) events" optional :note_events, type: Boolean, desc: "Trigger hook on note(comment) events"
......
...@@ -260,8 +260,9 @@ module API ...@@ -260,8 +260,9 @@ module API
class ProjectService < Grape::Entity class ProjectService < Grape::Entity
expose :id, :title, :created_at, :updated_at, :active expose :id, :title, :created_at, :updated_at, :active
expose :push_events, :issues_events, :merge_requests_events expose :push_events, :issues_events, :confidential_issues_events
expose :tag_push_events, :note_events, :pipeline_events expose :merge_requests_events, :tag_push_events, :note_events
expose :pipeline_events
expose :job_events, as: :build_events expose :job_events, as: :build_events
# Expose serialized properties # Expose serialized properties
expose :properties do |service, options| expose :properties do |service, options|
...@@ -270,8 +271,9 @@ module API ...@@ -270,8 +271,9 @@ module API
end end
class ProjectHook < ::API::Entities::Hook class ProjectHook < ::API::Entities::Hook
expose :project_id, :issues_events, :merge_requests_events expose :project_id, :issues_events, :confidential_issues_events
expose :note_events, :pipeline_events, :wiki_page_events expose :merge_requests_events, :note_events, :pipeline_events
expose :wiki_page_events
expose :job_events, as: :build_events expose :job_events, as: :build_events
end end
......
...@@ -11,6 +11,7 @@ module API ...@@ -11,6 +11,7 @@ module API
requires :url, type: String, desc: "The URL to send the request to" requires :url, type: String, desc: "The URL to send the request to"
optional :push_events, type: Boolean, desc: "Trigger hook on push events" optional :push_events, type: Boolean, desc: "Trigger hook on push events"
optional :issues_events, type: Boolean, desc: "Trigger hook on issues events" optional :issues_events, type: Boolean, desc: "Trigger hook on issues events"
optional :confidential_issues_events, type: Boolean, desc: "Trigger hook on confidential issues events"
optional :merge_requests_events, type: Boolean, desc: "Trigger hook on merge request events" optional :merge_requests_events, type: Boolean, desc: "Trigger hook on merge request events"
optional :tag_push_events, type: Boolean, desc: "Trigger hook on tag push events" optional :tag_push_events, type: Boolean, desc: "Trigger hook on tag push events"
optional :note_events, type: Boolean, desc: "Trigger hook on note(comment) events" optional :note_events, type: Boolean, desc: "Trigger hook on note(comment) events"
......
module Gitlab
module Ci
module Pipeline
module Expression
module Lexeme
class Base
def evaluate(**variables)
raise NotImplementedError
end
def self.build(token)
raise NotImplementedError
end
def self.scan(scanner)
if scanner.scan(self::PATTERN)
Expression::Token.new(scanner.matched, self)
end
end
end
end
end
end
end
end
module Gitlab
module Ci
module Pipeline
module Expression
module Lexeme
class Equals < Lexeme::Operator
PATTERN = /==/.freeze
def initialize(left, right)
@left = left
@right = right
end
def evaluate(variables = {})
@left.evaluate(variables) == @right.evaluate(variables)
end
def self.build(_value, behind, ahead)
new(behind, ahead)
end
end
end
end
end
end
end
module Gitlab
module Ci
module Pipeline
module Expression
module Lexeme
class Null < Lexeme::Value
PATTERN = /null/.freeze
def initialize(value = nil)
@value = nil
end
def evaluate(variables = {})
nil
end
def self.build(_value)
self.new
end
end
end
end
end
end
end
module Gitlab
module Ci
module Pipeline
module Expression
module Lexeme
class Operator < Lexeme::Base
def self.type
:operator
end
end
end
end
end
end
end
module Gitlab
module Ci
module Pipeline
module Expression
module Lexeme
class String < Lexeme::Value
PATTERN = /("(?<string>.+?)")|('(?<string>.+?)')/.freeze
def initialize(value)
@value = value
end
def evaluate(variables = {})
@value.to_s
end
def self.build(string)
new(string.match(PATTERN)[:string])
end
end
end
end
end
end
end
module Gitlab
module Ci
module Pipeline
module Expression
module Lexeme
class Value < Lexeme::Base
def self.type
:value
end
end
end
end
end
end
end
module Gitlab
module Ci
module Pipeline
module Expression
module Lexeme
class Variable < Lexeme::Value
PATTERN = /\$(?<name>\w+)/.freeze
def initialize(name)
@name = name
end
def evaluate(variables = {})
HashWithIndifferentAccess.new(variables).fetch(@name, nil)
end
def self.build(string)
new(string.match(PATTERN)[:name])
end
end
end
end
end
end
end
module Gitlab
module Ci
module Pipeline
module Expression
class Lexer
include ::Gitlab::Utils::StrongMemoize
LEXEMES = [
Expression::Lexeme::Variable,
Expression::Lexeme::String,
Expression::Lexeme::Null,
Expression::Lexeme::Equals
].freeze
SyntaxError = Class.new(Statement::StatementError)
MAX_TOKENS = 100
def initialize(statement, max_tokens: MAX_TOKENS)
@scanner = StringScanner.new(statement)
@max_tokens = max_tokens
end
def tokens
strong_memoize(:tokens) { tokenize }
end
def lexemes
tokens.map(&:to_lexeme)
end
private
def tokenize
tokens = []
@max_tokens.times do
@scanner.skip(/\s+/) # ignore whitespace
return tokens if @scanner.eos?
lexeme = LEXEMES.find do |type|
type.scan(@scanner).tap do |token|
tokens.push(token) if token.present?
end
end
unless lexeme.present?
raise Lexer::SyntaxError, 'Unknown lexeme found!'
end
end
raise Lexer::SyntaxError, 'Too many tokens!'
end
end
end
end
end
end
module Gitlab
module Ci
module Pipeline
module Expression
class Parser
def initialize(tokens)
@tokens = tokens.to_enum
@nodes = []
end
##
# This produces a reverse descent parse tree.
#
# It currently does not support precedence of operators.
#
def tree
while token = @tokens.next
case token.type
when :operator
token.build(@nodes.pop, tree).tap do |node|
@nodes.push(node)
end
when :value
token.build.tap do |leaf|
@nodes.push(leaf)
end
end
end
rescue StopIteration
@nodes.last || Lexeme::Null.new
end
def self.seed(statement)
new(Expression::Lexer.new(statement).tokens)
end
end
end
end
end
end
module Gitlab
module Ci
module Pipeline
module Expression
class Statement
StatementError = Class.new(StandardError)
GRAMMAR = [
%w[variable equals string],
%w[variable equals variable],
%w[variable equals null],
%w[string equals variable],
%w[null equals variable],
%w[variable]
].freeze
def initialize(statement, pipeline)
@lexer = Expression::Lexer.new(statement)
@variables = pipeline.variables.map do |variable|
[variable.key, variable.value]
end
end
def parse_tree
raise StatementError if @lexer.lexemes.empty?
unless GRAMMAR.find { |syntax| syntax == @lexer.lexemes }
raise StatementError, 'Unknown pipeline expression!'
end
Expression::Parser.new(@lexer.tokens).tree
end
def evaluate
parse_tree.evaluate(@variables.to_h)
end
end
end
end
end
end
module Gitlab
module Ci
module Pipeline
module Expression
class Token
attr_reader :value, :lexeme
def initialize(value, lexeme)
@value = value
@lexeme = lexeme
end
def build(*args)
@lexeme.build(@value, *args)
end
def type
@lexeme.type
end
def to_lexeme
@lexeme.name.demodulize.downcase
end
end
end
end
end
end
...@@ -467,7 +467,8 @@ module Gitlab ...@@ -467,7 +467,8 @@ module Gitlab
follow: false, follow: false,
skip_merges: false, skip_merges: false,
after: nil, after: nil,
before: nil before: nil,
all: false
} }
options = default_options.merge(options) options = default_options.merge(options)
...@@ -478,8 +479,9 @@ module Gitlab ...@@ -478,8 +479,9 @@ module Gitlab
raise ArgumentError.new("invalid Repository#log limit: #{limit.inspect}") raise ArgumentError.new("invalid Repository#log limit: #{limit.inspect}")
end end
# TODO support options[:all] in Gitaly https://gitlab.com/gitlab-org/gitaly/issues/1049
gitaly_migrate(:find_commits) do |is_enabled| gitaly_migrate(:find_commits) do |is_enabled|
if is_enabled if is_enabled && !options[:all]
gitaly_commit_client.find_commits(options) gitaly_commit_client.find_commits(options)
else else
raw_log(options).map { |c| Commit.decorate(self, c) } raw_log(options).map { |c| Commit.decorate(self, c) }
...@@ -489,13 +491,16 @@ module Gitlab ...@@ -489,13 +491,16 @@ module Gitlab
# Used in gitaly-ruby # Used in gitaly-ruby
def raw_log(options) def raw_log(options)
actual_ref = options[:ref] || root_ref sha =
begin unless options[:all]
sha = sha_from_ref(actual_ref) actual_ref = options[:ref] || root_ref
rescue Rugged::OdbError, Rugged::InvalidError, Rugged::ReferenceError begin
# Return an empty array if the ref wasn't found sha_from_ref(actual_ref)
return [] rescue Rugged::OdbError, Rugged::InvalidError, Rugged::ReferenceError
end # Return an empty array if the ref wasn't found
return []
end
end
log_by_shell(sha, options) log_by_shell(sha, options)
end end
...@@ -503,8 +508,9 @@ module Gitlab ...@@ -503,8 +508,9 @@ module Gitlab
def count_commits(options) def count_commits(options)
count_commits_options = process_count_commits_options(options) count_commits_options = process_count_commits_options(options)
# TODO add support for options[:all] in Gitaly https://gitlab.com/gitlab-org/gitaly/issues/1050
gitaly_migrate(:count_commits) do |is_enabled| gitaly_migrate(:count_commits) do |is_enabled|
if is_enabled if is_enabled && !options[:all]
count_commits_by_gitaly(count_commits_options) count_commits_by_gitaly(count_commits_options)
else else
count_commits_by_shelling_out(count_commits_options) count_commits_by_shelling_out(count_commits_options)
...@@ -1705,7 +1711,12 @@ module Gitlab ...@@ -1705,7 +1711,12 @@ module Gitlab
cmd << '--no-merges' if options[:skip_merges] cmd << '--no-merges' if options[:skip_merges]
cmd << "--after=#{options[:after].iso8601}" if options[:after] cmd << "--after=#{options[:after].iso8601}" if options[:after]
cmd << "--before=#{options[:before].iso8601}" if options[:before] cmd << "--before=#{options[:before].iso8601}" if options[:before]
cmd << sha
if options[:all]
cmd += %w[--all --reverse]
else
cmd << sha
end
# :path can be a string or an array of strings # :path can be a string or an array of strings
if options[:path].present? if options[:path].present?
...@@ -1922,7 +1933,16 @@ module Gitlab ...@@ -1922,7 +1933,16 @@ module Gitlab
cmd << "--before=#{options[:before].iso8601}" if options[:before] cmd << "--before=#{options[:before].iso8601}" if options[:before]
cmd << "--max-count=#{options[:max_count]}" if options[:max_count] cmd << "--max-count=#{options[:max_count]}" if options[:max_count]
cmd << "--left-right" if options[:left_right] cmd << "--left-right" if options[:left_right]
cmd += %W[--count #{options[:ref]}] cmd << '--count'
cmd << if options[:all]
'--all'
elsif options[:ref]
options[:ref]
else
raise ArgumentError, "Please specify a valid ref or set the 'all' attribute to true"
end
cmd += %W[-- #{options[:path]}] if options[:path].present? cmd += %W[-- #{options[:path]}] if options[:path].present?
cmd cmd
end end
......
...@@ -41,7 +41,7 @@ module Gitlab ...@@ -41,7 +41,7 @@ module Gitlab
end end
def apply_gitattributes(revision) def apply_gitattributes(revision)
request = Gitaly::ApplyGitattributesRequest.new(repository: @gitaly_repo, revision: revision) request = Gitaly::ApplyGitattributesRequest.new(repository: @gitaly_repo, revision: encode_binary(revision))
GitalyClient.call(@storage, :repository_service, :apply_gitattributes, request) GitalyClient.call(@storage, :repository_service, :apply_gitattributes, request)
end end
......
...@@ -134,5 +134,15 @@ describe 'Profile > Password' do ...@@ -134,5 +134,15 @@ describe 'Profile > Password' do
expect(current_path).to eq new_user_session_path expect(current_path).to eq new_user_session_path
end end
context 'when global require_two_factor_authentication is enabled' do
it 'needs change user password' do
stub_application_setting(require_two_factor_authentication: true)
visit profile_path
expect(current_path).to eq new_profile_password_path
end
end
end end
end end
require 'spec_helper'
describe Gitlab::Ci::Pipeline::Expression::Lexeme::Equals do
let(:left) { double('left') }
let(:right) { double('right') }
describe '.build' do
it 'creates a new instance of the token' do
expect(described_class.build('==', left, right))
.to be_a(described_class)
end
end
describe '.type' do
it 'is an operator' do
expect(described_class.type).to eq :operator
end
end
describe '#evaluate' do
it 'returns false when left and right are not equal' do
allow(left).to receive(:evaluate).and_return(1)
allow(right).to receive(:evaluate).and_return(2)
operator = described_class.new(left, right)
expect(operator.evaluate(VARIABLE: 3)).to eq false
end
it 'returns true when left and right are equal' do
allow(left).to receive(:evaluate).and_return(1)
allow(right).to receive(:evaluate).and_return(1)
operator = described_class.new(left, right)
expect(operator.evaluate(VARIABLE: 3)).to eq true
end
end
end
require 'spec_helper'
describe Gitlab::Ci::Pipeline::Expression::Lexeme::Null do
describe '.build' do
it 'creates a new instance of the token' do
expect(described_class.build('null'))
.to be_a(described_class)
end
end
describe '.type' do
it 'is a value lexeme' do
expect(described_class.type).to eq :value
end
end
describe '#evaluate' do
it 'always evaluates to `nil`' do
expect(described_class.new('null').evaluate).to be_nil
end
end
end
require 'spec_helper'
describe Gitlab::Ci::Pipeline::Expression::Lexeme::String do
describe '.build' do
it 'creates a new instance of the token' do
expect(described_class.build('"my string"'))
.to be_a(described_class)
end
end
describe '.type' do
it 'is a value lexeme' do
expect(described_class.type).to eq :value
end
end
describe '.scan' do
context 'when using double quotes' do
it 'correctly identifies string token' do
scanner = StringScanner.new('"some string"')
token = described_class.scan(scanner)
expect(token).not_to be_nil
expect(token.build.evaluate).to eq 'some string'
end
end
context 'when using single quotes' do
it 'correctly identifies string token' do
scanner = StringScanner.new("'some string 2'")
token = described_class.scan(scanner)
expect(token).not_to be_nil
expect(token.build.evaluate).to eq 'some string 2'
end
end
context 'when there are mixed quotes in the string' do
it 'is a greedy scanner for double quotes' do
scanner = StringScanner.new('"some string" "and another one"')
token = described_class.scan(scanner)
expect(token).not_to be_nil
expect(token.build.evaluate).to eq 'some string'
end
it 'is a greedy scanner for single quotes' do
scanner = StringScanner.new("'some string' 'and another one'")
token = described_class.scan(scanner)
expect(token).not_to be_nil
expect(token.build.evaluate).to eq 'some string'
end
it 'allows to use single quotes inside double quotes' do
scanner = StringScanner.new(%("some ' string"))
token = described_class.scan(scanner)
expect(token).not_to be_nil
expect(token.build.evaluate).to eq "some ' string"
end
it 'allow to use double quotes inside single quotes' do
scanner = StringScanner.new(%('some " string'))
token = described_class.scan(scanner)
expect(token).not_to be_nil
expect(token.build.evaluate).to eq 'some " string'
end
end
end
describe '#evaluate' do
it 'returns string value it is is present' do
string = described_class.new('my string')
expect(string.evaluate).to eq 'my string'
end
it 'returns an empty string if it is empty' do
string = described_class.new('')
expect(string.evaluate).to eq ''
end
end
end
require 'spec_helper'
describe Gitlab::Ci::Pipeline::Expression::Lexeme::Variable do
describe '.build' do
it 'creates a new instance of the token' do
expect(described_class.build('$VARIABLE'))
.to be_a(described_class)
end
end
describe '.type' do
it 'is a value lexeme' do
expect(described_class.type).to eq :value
end
end
describe '#evaluate' do
it 'returns variable value if it is defined' do
variable = described_class.new('VARIABLE')
expect(variable.evaluate(VARIABLE: 'my variable'))
.to eq 'my variable'
end
it 'allows to use a string as a variable key too' do
variable = described_class.new('VARIABLE')
expect(variable.evaluate('VARIABLE' => 'my variable'))
.to eq 'my variable'
end
it 'returns nil if it is not defined' do
variable = described_class.new('VARIABLE')
expect(variable.evaluate(OTHER: 'variable')).to be_nil
end
it 'returns an empty string if it is empty' do
variable = described_class.new('VARIABLE')
expect(variable.evaluate(VARIABLE: '')).to eq ''
end
end
end
require 'spec_helper'
describe Gitlab::Ci::Pipeline::Expression::Lexer do
let(:token_class) do
Gitlab::Ci::Pipeline::Expression::Token
end
describe '#tokens' do
it 'tokenss single value' do
tokens = described_class.new('$VARIABLE').tokens
expect(tokens).to be_one
expect(tokens).to all(be_an_instance_of(token_class))
end
it 'does ignore whitespace characters' do
tokens = described_class.new("\t$VARIABLE ").tokens
expect(tokens).to be_one
expect(tokens).to all(be_an_instance_of(token_class))
end
it 'tokenss multiple values of the same token' do
tokens = described_class.new("$VARIABLE1 $VARIABLE2").tokens
expect(tokens.size).to eq 2
expect(tokens).to all(be_an_instance_of(token_class))
end
it 'tokenss multiple values with different tokens' do
tokens = described_class.new('$VARIABLE "text" "value"').tokens
expect(tokens.size).to eq 3
expect(tokens.first.value).to eq '$VARIABLE'
expect(tokens.second.value).to eq '"text"'
expect(tokens.third.value).to eq '"value"'
end
it 'tokenss tokens and operators' do
tokens = described_class.new('$VARIABLE == "text"').tokens
expect(tokens.size).to eq 3
expect(tokens.first.value).to eq '$VARIABLE'
expect(tokens.second.value).to eq '=='
expect(tokens.third.value).to eq '"text"'
end
it 'limits statement to specified amount of tokens' do
lexer = described_class.new("$V1 $V2 $V3 $V4", max_tokens: 3)
expect { lexer.tokens }
.to raise_error described_class::SyntaxError
end
it 'raises syntax error in case of finding unknown tokens' do
lexer = described_class.new('$V1 123 $V2')
expect { lexer.tokens }
.to raise_error described_class::SyntaxError
end
end
describe '#lexemes' do
it 'returns an array of syntax lexemes' do
lexer = described_class.new('$VAR "text"')
expect(lexer.lexemes).to eq %w[variable string]
end
end
end
require 'spec_helper'
describe Gitlab::Ci::Pipeline::Expression::Parser do
describe '#tree' do
context 'when using operators' do
it 'returns a reverse descent parse tree' do
expect(described_class.seed('$VAR1 == "123" == $VAR2').tree)
.to be_a Gitlab::Ci::Pipeline::Expression::Lexeme::Equals
end
end
context 'when using a single token' do
it 'returns a single token instance' do
expect(described_class.seed('$VAR').tree)
.to be_a Gitlab::Ci::Pipeline::Expression::Lexeme::Variable
end
end
context 'when expression is empty' do
it 'returns a null token' do
expect(described_class.seed('').tree)
.to be_a Gitlab::Ci::Pipeline::Expression::Lexeme::Null
end
end
end
end
require 'spec_helper'
describe Gitlab::Ci::Pipeline::Expression::Statement do
let(:pipeline) { build(:ci_pipeline) }
subject do
described_class.new(text, pipeline)
end
before do
pipeline.variables.build([key: 'VARIABLE', value: 'my variable'])
end
describe '#parse_tree' do
context 'when expression is empty' do
let(:text) { '' }
it 'raises an error' do
expect { subject.parse_tree }
.to raise_error described_class::StatementError
end
end
context 'when expression grammar is incorrect' do
table = [
'$VAR "text"', # missing operator
'== "123"', # invalid right side
"'single quotes'", # single quotes string
'$VAR ==', # invalid right side
'12345', # unknown syntax
'' # empty statement
]
table.each do |syntax|
it "raises an error when syntax is `#{syntax}`" do
expect { described_class.new(syntax, pipeline).parse_tree }
.to raise_error described_class::StatementError
end
end
end
context 'when expression grammar is correct' do
context 'when using an operator' do
let(:text) { '$VAR == "value"' }
it 'returns a reverse descent parse tree' do
expect(subject.parse_tree)
.to be_a Gitlab::Ci::Pipeline::Expression::Lexeme::Equals
end
end
context 'when using a single token' do
let(:text) { '$VARIABLE' }
it 'returns a single token instance' do
expect(subject.parse_tree)
.to be_a Gitlab::Ci::Pipeline::Expression::Lexeme::Variable
end
end
end
end
describe '#evaluate' do
statements = [
['$VARIABLE == "my variable"', true],
["$VARIABLE == 'my variable'", true],
['"my variable" == $VARIABLE', true],
['$VARIABLE == null', false],
['$VAR == null', true],
['null == $VAR', true],
['$VARIABLE', 'my variable'],
['$VAR', nil]
]
statements.each do |expression, value|
context "when using expression `#{expression}`" do
let(:text) { expression }
it "evaluates to `#{value.inspect}`" do
expect(subject.evaluate).to eq value
end
end
end
end
end
require 'spec_helper'
describe Gitlab::Ci::Pipeline::Expression::Token do
let(:value) { '$VARIABLE' }
let(:lexeme) { Gitlab::Ci::Pipeline::Expression::Lexeme::Variable }
subject { described_class.new(value, lexeme) }
describe '#value' do
it 'returns raw token value' do
expect(subject.value).to eq value
end
end
describe '#lexeme' do
it 'returns raw token lexeme' do
expect(subject.lexeme).to eq lexeme
end
end
describe '#build' do
it 'delegates to lexeme after adding a value' do
expect(lexeme).to receive(:build)
.with(value, 'some', 'args')
subject.build('some', 'args')
end
it 'allows passing only required arguments' do
expect(subject.build).to be_an_instance_of(lexeme)
end
end
describe '#type' do
it 'delegates type query to the lexeme' do
expect(subject.type).to eq :value
end
end
describe '#to_lexeme' do
it 'returns raw lexeme syntax component name' do
expect(subject.to_lexeme).to eq 'variable'
end
end
end
...@@ -895,7 +895,7 @@ describe Gitlab::Git::Repository, seed_helper: true do ...@@ -895,7 +895,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
repository.log(options.merge(path: "encoding")) repository.log(options.merge(path: "encoding"))
end end
it "should not follow renames" do it "does not follow renames" do
expect(log_commits).to include(commit_with_new_name) expect(log_commits).to include(commit_with_new_name)
expect(log_commits).to include(rename_commit) expect(log_commits).to include(rename_commit)
expect(log_commits).not_to include(commit_with_old_name) expect(log_commits).not_to include(commit_with_old_name)
...@@ -907,7 +907,7 @@ describe Gitlab::Git::Repository, seed_helper: true do ...@@ -907,7 +907,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
repository.log(options.merge(path: "encoding/CHANGELOG")) repository.log(options.merge(path: "encoding/CHANGELOG"))
end end
it "should not follow renames" do it "does not follow renames" do
expect(log_commits).to include(commit_with_new_name) expect(log_commits).to include(commit_with_new_name)
expect(log_commits).to include(rename_commit) expect(log_commits).to include(rename_commit)
expect(log_commits).not_to include(commit_with_old_name) expect(log_commits).not_to include(commit_with_old_name)
...@@ -919,7 +919,7 @@ describe Gitlab::Git::Repository, seed_helper: true do ...@@ -919,7 +919,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
repository.log(options.merge(path: "CHANGELOG")) repository.log(options.merge(path: "CHANGELOG"))
end end
it "should not follow renames" do it "does not follow renames" do
expect(log_commits).to include(commit_with_old_name) expect(log_commits).to include(commit_with_old_name)
expect(log_commits).to include(rename_commit) expect(log_commits).to include(rename_commit)
expect(log_commits).not_to include(commit_with_new_name) expect(log_commits).not_to include(commit_with_new_name)
...@@ -931,7 +931,7 @@ describe Gitlab::Git::Repository, seed_helper: true do ...@@ -931,7 +931,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
repository.log(options.merge(ref: "refs/heads/fix-blob-path", path: "files/testdir/file.txt")) repository.log(options.merge(ref: "refs/heads/fix-blob-path", path: "files/testdir/file.txt"))
end end
it "should return a list of commits" do it "returns a list of commits" do
expect(log_commits.size).to eq(1) expect(log_commits.size).to eq(1)
end end
end end
...@@ -991,6 +991,16 @@ describe Gitlab::Git::Repository, seed_helper: true do ...@@ -991,6 +991,16 @@ describe Gitlab::Git::Repository, seed_helper: true do
it { expect { repository.log(limit: limit) }.to raise_error(ArgumentError) } it { expect { repository.log(limit: limit) }.to raise_error(ArgumentError) }
end end
end end
context 'with all' do
let(:options) { { all: true, limit: 50 } }
it 'returns a list of commits' do
commits = repository.log(options)
expect(commits.size).to eq(37)
end
end
end end
describe "#rugged_commits_between" do describe "#rugged_commits_between" do
...@@ -1134,6 +1144,20 @@ describe Gitlab::Git::Repository, seed_helper: true do ...@@ -1134,6 +1144,20 @@ describe Gitlab::Git::Repository, seed_helper: true do
context 'when Gitaly count_commits feature is disabled', :skip_gitaly_mock do context 'when Gitaly count_commits feature is disabled', :skip_gitaly_mock do
it_behaves_like 'extended commit counting' it_behaves_like 'extended commit counting'
context "with all" do
it "returns the number of commits in the whole repository" do
options = { all: true }
expect(repository.count_commits(options)).to eq(34)
end
end
context 'without all or ref being specified' do
it "raises an ArgumentError" do
expect { repository.count_commits({}) }.to raise_error(ArgumentError, "Please specify a valid ref or set the 'all' attribute to true")
end
end
end end
end end
...@@ -1406,79 +1430,95 @@ describe Gitlab::Git::Repository, seed_helper: true do ...@@ -1406,79 +1430,95 @@ describe Gitlab::Git::Repository, seed_helper: true do
end end
describe "#copy_gitattributes" do describe "#copy_gitattributes" do
let(:attributes_path) { File.join(SEED_STORAGE_PATH, TEST_REPO_PATH, 'info/attributes') } shared_examples 'applying git attributes' do
let(:attributes_path) { File.join(SEED_STORAGE_PATH, TEST_REPO_PATH, 'info/attributes') }
it "raises an error with invalid ref" do after do
expect { repository.copy_gitattributes("invalid") }.to raise_error(Gitlab::Git::Repository::InvalidRef) FileUtils.rm_rf(attributes_path) if Dir.exist?(attributes_path)
end
context "with no .gitattrbutes" do
before do
repository.copy_gitattributes("master")
end end
it "does not have an info/attributes" do it "raises an error with invalid ref" do
expect(File.exist?(attributes_path)).to be_falsey expect { repository.copy_gitattributes("invalid") }.to raise_error(Gitlab::Git::Repository::InvalidRef)
end end
after do context 'when forcing encoding issues' do
FileUtils.rm_rf(attributes_path) let(:branch_name) { "ʕ•ᴥ•ʔ" }
end
end
context "with .gitattrbutes" do before do
before do repository.create_branch(branch_name, "master")
repository.copy_gitattributes("gitattributes") end
end
it "has an info/attributes" do after do
expect(File.exist?(attributes_path)).to be_truthy repository.rm_branch(branch_name, user: build(:admin))
end end
it "has the same content in info/attributes as .gitattributes" do it "doesn't raise with a valid unicode ref" do
contents = File.open(attributes_path, "rb") { |f| f.read } expect { repository.copy_gitattributes(branch_name) }.not_to raise_error
expect(contents).to eq("*.md binary\n")
end
after do repository
FileUtils.rm_rf(attributes_path) end
end end
end
context "with updated .gitattrbutes" do context "with no .gitattrbutes" do
before do before do
repository.copy_gitattributes("gitattributes") repository.copy_gitattributes("master")
repository.copy_gitattributes("gitattributes-updated") end
end
it "has an info/attributes" do it "does not have an info/attributes" do
expect(File.exist?(attributes_path)).to be_truthy expect(File.exist?(attributes_path)).to be_falsey
end
end end
it "has the updated content in info/attributes" do context "with .gitattrbutes" do
contents = File.read(attributes_path) before do
expect(contents).to eq("*.txt binary\n") repository.copy_gitattributes("gitattributes")
end end
after do it "has an info/attributes" do
FileUtils.rm_rf(attributes_path) expect(File.exist?(attributes_path)).to be_truthy
end end
end
context "with no .gitattrbutes in HEAD but with previous info/attributes" do it "has the same content in info/attributes as .gitattributes" do
before do contents = File.open(attributes_path, "rb") { |f| f.read }
repository.copy_gitattributes("gitattributes") expect(contents).to eq("*.md binary\n")
repository.copy_gitattributes("master") end
end end
it "does not have an info/attributes" do context "with updated .gitattrbutes" do
expect(File.exist?(attributes_path)).to be_falsey before do
repository.copy_gitattributes("gitattributes")
repository.copy_gitattributes("gitattributes-updated")
end
it "has an info/attributes" do
expect(File.exist?(attributes_path)).to be_truthy
end
it "has the updated content in info/attributes" do
contents = File.read(attributes_path)
expect(contents).to eq("*.txt binary\n")
end
end end
after do context "with no .gitattrbutes in HEAD but with previous info/attributes" do
FileUtils.rm_rf(attributes_path) before do
repository.copy_gitattributes("gitattributes")
repository.copy_gitattributes("master")
end
it "does not have an info/attributes" do
expect(File.exist?(attributes_path)).to be_falsey
end
end end
end end
context 'when gitaly is enabled' do
it_behaves_like 'applying git attributes'
end
context 'when gitaly is disabled', :disable_gitaly do
it_behaves_like 'applying git attributes'
end
end end
describe '#ref_exists?' do describe '#ref_exists?' do
......
...@@ -29,6 +29,7 @@ describe Gitlab::ImportExport::RelationFactory do ...@@ -29,6 +29,7 @@ describe Gitlab::ImportExport::RelationFactory do
'service_id' => service_id, 'service_id' => service_id,
'push_events' => true, 'push_events' => true,
'issues_events' => false, 'issues_events' => false,
'confidential_issues_events' => false,
'merge_requests_events' => true, 'merge_requests_events' => true,
'tag_push_events' => false, 'tag_push_events' => false,
'note_events' => true, 'note_events' => true,
......
require 'spec_helper' require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20170502101023_cleanup_namespaceless_pending_delete_projects.rb') require Rails.root.join('db', 'post_migrate', '20170502101023_cleanup_namespaceless_pending_delete_projects.rb')
describe CleanupNamespacelessPendingDeleteProjects do describe CleanupNamespacelessPendingDeleteProjects, :migration, schema: 20180222043024 do
before do before do
# Stub after_save callbacks that will fail when Project has no namespace # Stub after_save callbacks that will fail when Project has no namespace
allow_any_instance_of(Project).to receive(:ensure_storage_path_exists).and_return(nil) allow_any_instance_of(Project).to receive(:ensure_storage_path_exists).and_return(nil)
......
...@@ -8,7 +8,7 @@ describe MigrateIssuesToGhostUser, :migration do ...@@ -8,7 +8,7 @@ describe MigrateIssuesToGhostUser, :migration do
let(:users) { table(:users) } let(:users) { table(:users) }
before do before do
project = projects.create!(name: 'gitlab') project = projects.create!(name: 'gitlab', namespace_id: 1)
user = users.create(email: 'test@example.com') user = users.create(email: 'test@example.com')
issues.create(title: 'Issue 1', author_id: nil, project_id: project.id) issues.create(title: 'Issue 1', author_id: nil, project_id: project.id)
issues.create(title: 'Issue 2', author_id: user.id, project_id: project.id) issues.create(title: 'Issue 2', author_id: user.id, project_id: project.id)
......
...@@ -2909,7 +2909,8 @@ describe Project do ...@@ -2909,7 +2909,8 @@ describe Project do
end end
it 'is a no-op when there is no namespace' do it 'is a no-op when there is no namespace' do
project.update_column(:namespace_id, nil) project.namespace.delete
project.reload
expect_any_instance_of(Projects::UpdatePagesConfigurationService).not_to receive(:execute) expect_any_instance_of(Projects::UpdatePagesConfigurationService).not_to receive(:execute)
expect_any_instance_of(Gitlab::PagesTransfer).not_to receive(:rename_project) expect_any_instance_of(Gitlab::PagesTransfer).not_to receive(:rename_project)
...@@ -2941,7 +2942,8 @@ describe Project do ...@@ -2941,7 +2942,8 @@ describe Project do
it 'is a no-op on legacy projects when there is no namespace' do it 'is a no-op on legacy projects when there is no namespace' do
export_path = legacy_project.export_path export_path = legacy_project.export_path
legacy_project.update_column(:namespace_id, nil) legacy_project.namespace.delete
legacy_project.reload
expect(FileUtils).not_to receive(:rm_rf).with(export_path) expect(FileUtils).not_to receive(:rm_rf).with(export_path)
...@@ -2953,7 +2955,8 @@ describe Project do ...@@ -2953,7 +2955,8 @@ describe Project do
it 'runs on hashed storage projects when there is no namespace' do it 'runs on hashed storage projects when there is no namespace' do
export_path = project.export_path export_path = project.export_path
project.update_column(:namespace_id, nil) project.namespace.delete
legacy_project.reload
allow(FileUtils).to receive(:rm_rf).and_call_original allow(FileUtils).to receive(:rm_rf).and_call_original
expect(FileUtils).to receive(:rm_rf).with(export_path).and_call_original expect(FileUtils).to receive(:rm_rf).with(export_path).and_call_original
......
...@@ -242,23 +242,51 @@ describe Repository do ...@@ -242,23 +242,51 @@ describe Repository do
end end
describe '#commits' do describe '#commits' do
it 'sets follow when path is a single path' do context 'when neither the all flag nor a ref are specified' do
expect(Gitlab::Git::Commit).to receive(:where).with(a_hash_including(follow: true)).and_call_original.twice it 'returns every commit from default branch' do
expect(repository.commits(limit: 60).size).to eq(37)
repository.commits('master', limit: 1, path: 'README.md') end
repository.commits('master', limit: 1, path: ['README.md'])
end end
it 'does not set follow when path is multiple paths' do context 'when ref is passed' do
expect(Gitlab::Git::Commit).to receive(:where).with(a_hash_including(follow: false)).and_call_original it 'returns every commit from the specified ref' do
expect(repository.commits('master', limit: 60).size).to eq(37)
end
repository.commits('master', limit: 1, path: ['README.md', 'CHANGELOG']) context 'when all' do
end it 'returns every commit from the repository' do
expect(repository.commits('master', limit: 60, all: true).size).to eq(60)
end
end
context 'with path' do
it 'sets follow when it is a single path' do
expect(Gitlab::Git::Commit).to receive(:where).with(a_hash_including(follow: true)).and_call_original.twice
repository.commits('master', limit: 1, path: 'README.md')
repository.commits('master', limit: 1, path: ['README.md'])
end
it 'does not set follow when there are no paths' do it 'does not set follow when it is multiple paths' do
expect(Gitlab::Git::Commit).to receive(:where).with(a_hash_including(follow: false)).and_call_original expect(Gitlab::Git::Commit).to receive(:where).with(a_hash_including(follow: false)).and_call_original
repository.commits('master', limit: 1) repository.commits('master', limit: 1, path: ['README.md', 'CHANGELOG'])
end
end
context 'without path' do
it 'does not set follow' do
expect(Gitlab::Git::Commit).to receive(:where).with(a_hash_including(follow: false)).and_call_original
repository.commits('master', limit: 1)
end
end
end
context "when 'all' flag is set" do
it 'returns every commit from the repository' do
expect(repository.commits(all: true, limit: 60).size).to eq(60)
end
end end
end end
......
...@@ -149,6 +149,18 @@ describe API::Commits do ...@@ -149,6 +149,18 @@ describe API::Commits do
end end
end end
context 'all optional parameter' do
it 'returns all project commits' do
commit_count = project.repository.count_commits(all: true)
get api("/projects/#{project_id}/repository/commits?all=true", user)
expect(response).to include_pagination_headers
expect(response.headers['X-Total']).to eq(commit_count.to_s)
expect(response.headers['X-Page']).to eql('1')
end
end
context 'with pagination params' do context 'with pagination params' do
let(:page) { 1 } let(:page) { 1 }
let(:per_page) { 5 } let(:per_page) { 5 }
......
...@@ -28,6 +28,7 @@ describe API::ProjectHooks, 'ProjectHooks' do ...@@ -28,6 +28,7 @@ describe API::ProjectHooks, 'ProjectHooks' do
expect(json_response.count).to eq(1) expect(json_response.count).to eq(1)
expect(json_response.first['url']).to eq("http://example.com") expect(json_response.first['url']).to eq("http://example.com")
expect(json_response.first['issues_events']).to eq(true) expect(json_response.first['issues_events']).to eq(true)
expect(json_response.first['confidential_issues_events']).to eq(true)
expect(json_response.first['push_events']).to eq(true) expect(json_response.first['push_events']).to eq(true)
expect(json_response.first['merge_requests_events']).to eq(true) expect(json_response.first['merge_requests_events']).to eq(true)
expect(json_response.first['tag_push_events']).to eq(true) expect(json_response.first['tag_push_events']).to eq(true)
...@@ -56,6 +57,7 @@ describe API::ProjectHooks, 'ProjectHooks' do ...@@ -56,6 +57,7 @@ describe API::ProjectHooks, 'ProjectHooks' do
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
expect(json_response['url']).to eq(hook.url) expect(json_response['url']).to eq(hook.url)
expect(json_response['issues_events']).to eq(hook.issues_events) expect(json_response['issues_events']).to eq(hook.issues_events)
expect(json_response['confidential_issues_events']).to eq(hook.confidential_issues_events)
expect(json_response['push_events']).to eq(hook.push_events) expect(json_response['push_events']).to eq(hook.push_events)
expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events) expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events)
expect(json_response['tag_push_events']).to eq(hook.tag_push_events) expect(json_response['tag_push_events']).to eq(hook.tag_push_events)
...@@ -90,13 +92,14 @@ describe API::ProjectHooks, 'ProjectHooks' do ...@@ -90,13 +92,14 @@ describe API::ProjectHooks, 'ProjectHooks' do
it "adds hook to project" do it "adds hook to project" do
expect do expect do
post api("/projects/#{project.id}/hooks", user), post api("/projects/#{project.id}/hooks", user),
url: "http://example.com", issues_events: true, wiki_page_events: true, url: "http://example.com", issues_events: true, confidential_issues_events: true, wiki_page_events: true,
job_events: true job_events: true
end.to change {project.hooks.count}.by(1) end.to change {project.hooks.count}.by(1)
expect(response).to have_gitlab_http_status(201) expect(response).to have_gitlab_http_status(201)
expect(json_response['url']).to eq('http://example.com') expect(json_response['url']).to eq('http://example.com')
expect(json_response['issues_events']).to eq(true) expect(json_response['issues_events']).to eq(true)
expect(json_response['confidential_issues_events']).to eq(true)
expect(json_response['push_events']).to eq(true) expect(json_response['push_events']).to eq(true)
expect(json_response['merge_requests_events']).to eq(false) expect(json_response['merge_requests_events']).to eq(false)
expect(json_response['tag_push_events']).to eq(false) expect(json_response['tag_push_events']).to eq(false)
...@@ -144,6 +147,7 @@ describe API::ProjectHooks, 'ProjectHooks' do ...@@ -144,6 +147,7 @@ describe API::ProjectHooks, 'ProjectHooks' do
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
expect(json_response['url']).to eq('http://example.org') expect(json_response['url']).to eq('http://example.org')
expect(json_response['issues_events']).to eq(hook.issues_events) expect(json_response['issues_events']).to eq(hook.issues_events)
expect(json_response['confidential_issues_events']).to eq(hook.confidential_issues_events)
expect(json_response['push_events']).to eq(false) expect(json_response['push_events']).to eq(false)
expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events) expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events)
expect(json_response['tag_push_events']).to eq(hook.tag_push_events) expect(json_response['tag_push_events']).to eq(hook.tag_push_events)
......
...@@ -27,6 +27,7 @@ describe API::ProjectHooks, 'ProjectHooks' do ...@@ -27,6 +27,7 @@ describe API::ProjectHooks, 'ProjectHooks' do
expect(json_response.count).to eq(1) expect(json_response.count).to eq(1)
expect(json_response.first['url']).to eq("http://example.com") expect(json_response.first['url']).to eq("http://example.com")
expect(json_response.first['issues_events']).to eq(true) expect(json_response.first['issues_events']).to eq(true)
expect(json_response.first['confidential_issues_events']).to eq(true)
expect(json_response.first['push_events']).to eq(true) expect(json_response.first['push_events']).to eq(true)
expect(json_response.first['merge_requests_events']).to eq(true) expect(json_response.first['merge_requests_events']).to eq(true)
expect(json_response.first['tag_push_events']).to eq(true) expect(json_response.first['tag_push_events']).to eq(true)
...@@ -54,6 +55,7 @@ describe API::ProjectHooks, 'ProjectHooks' do ...@@ -54,6 +55,7 @@ describe API::ProjectHooks, 'ProjectHooks' do
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
expect(json_response['url']).to eq(hook.url) expect(json_response['url']).to eq(hook.url)
expect(json_response['issues_events']).to eq(hook.issues_events) expect(json_response['issues_events']).to eq(hook.issues_events)
expect(json_response['confidential_issues_events']).to eq(hook.confidential_issues_events)
expect(json_response['push_events']).to eq(hook.push_events) expect(json_response['push_events']).to eq(hook.push_events)
expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events) expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events)
expect(json_response['tag_push_events']).to eq(hook.tag_push_events) expect(json_response['tag_push_events']).to eq(hook.tag_push_events)
...@@ -87,12 +89,13 @@ describe API::ProjectHooks, 'ProjectHooks' do ...@@ -87,12 +89,13 @@ describe API::ProjectHooks, 'ProjectHooks' do
it "adds hook to project" do it "adds hook to project" do
expect do expect do
post v3_api("/projects/#{project.id}/hooks", user), post v3_api("/projects/#{project.id}/hooks", user),
url: "http://example.com", issues_events: true, wiki_page_events: true, build_events: true url: "http://example.com", issues_events: true, confidential_issues_events: true, wiki_page_events: true, build_events: true
end.to change {project.hooks.count}.by(1) end.to change {project.hooks.count}.by(1)
expect(response).to have_gitlab_http_status(201) expect(response).to have_gitlab_http_status(201)
expect(json_response['url']).to eq('http://example.com') expect(json_response['url']).to eq('http://example.com')
expect(json_response['issues_events']).to eq(true) expect(json_response['issues_events']).to eq(true)
expect(json_response['confidential_issues_events']).to eq(true)
expect(json_response['push_events']).to eq(true) expect(json_response['push_events']).to eq(true)
expect(json_response['merge_requests_events']).to eq(false) expect(json_response['merge_requests_events']).to eq(false)
expect(json_response['tag_push_events']).to eq(false) expect(json_response['tag_push_events']).to eq(false)
...@@ -139,6 +142,7 @@ describe API::ProjectHooks, 'ProjectHooks' do ...@@ -139,6 +142,7 @@ describe API::ProjectHooks, 'ProjectHooks' do
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
expect(json_response['url']).to eq('http://example.org') expect(json_response['url']).to eq('http://example.org')
expect(json_response['issues_events']).to eq(hook.issues_events) expect(json_response['issues_events']).to eq(hook.issues_events)
expect(json_response['confidential_issues_events']).to eq(hook.confidential_issues_events)
expect(json_response['push_events']).to eq(false) expect(json_response['push_events']).to eq(false)
expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events) expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events)
expect(json_response['tag_push_events']).to eq(hook.tag_push_events) expect(json_response['tag_push_events']).to eq(hook.tag_push_events)
......
# pack-refs with: peeled fully-peeled # pack-refs with: peeled fully-peeled sorted
0b4bc9a49b562e85de7cc9e834518ea6828729b9 refs/heads/feature 0b4bc9a49b562e85de7cc9e834518ea6828729b9 refs/heads/feature
12d65c8dd2b2676fa3ac47d955accc085a37a9c1 refs/heads/fix 12d65c8dd2b2676fa3ac47d955accc085a37a9c1 refs/heads/fix
6473c90867124755509e100d0d35ebdc85a0b6ae refs/heads/fix-blob-path 6473c90867124755509e100d0d35ebdc85a0b6ae refs/heads/fix-blob-path
......
...@@ -22,7 +22,9 @@ describe NamespacelessProjectDestroyWorker do ...@@ -22,7 +22,9 @@ describe NamespacelessProjectDestroyWorker do
end end
end end
context 'project has no namespace' do # Only possible with schema 20180222043024 and lower.
# Project#namespace_id has not null constraint since then
context 'project has no namespace', :migration, schema: 20180222043024 do
let!(:project) do let!(:project) do
project = build(:project, namespace_id: nil) project = build(:project, namespace_id: nil)
project.save(validate: false) project.save(validate: false)
......
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