Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions app/services/search/query_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ def apply_selector(node, relation)
apply_last_date_selector(:>=, value, relation, negated: negated)
when :last_before
apply_last_date_selector(:<, value, relation, negated: negated)
when :commitfest
apply_commitfest_selector(value, relation, negated: negated, conditions: conditions)
else
@warnings << "Unknown selector: #{key}"
relation
Expand Down Expand Up @@ -842,6 +844,53 @@ def apply_last_date_selector(operator, value, relation, negated:)
relation.where("topics.last_message_at #{actual_operator} ?", date)
end

def apply_commitfest_selector(value, relation, negated:, conditions:)
relation = relation.left_joins(
commitfest_patch_topics: {
commitfest_patch: [
:commitfest_tags,
{ commitfest_patch_commitfests: :commitfest }
]
}
)

if value.present?
if negated
relation = relation.where.not(commitfest: { name: value })
else
relation = relation.where(commitfest: { name: value })
end
end

apply_commitfest_with_conditions(relation, negated: negated, conditions: conditions || [])
end

def apply_commitfest_with_conditions(relation, negated:, conditions:)
conditions.each do |condition|
value = condition[:value]

relation = case condition[:key]
when :name
column = Commitfest.arel_table[:name]
apply_commitfest_filter(relation, column, value, negated:)
when :status
column = CommitfestPatchCommitfest.arel_table[:status]
apply_commitfest_filter(relation, column, value, negated:)
when :tag
column = CommitfestTag.arel_table[:name]
apply_commitfest_filter(relation, column, value, negated:)
end
end

relation
end

def apply_commitfest_filter(relation, column, value, negated:)
predicate = column.lower.eq(value.downcase)
predicate = predicate.not if negated
relation.where(predicate)
end

# === Helpers ===

def sanitize_like(value)
Expand Down
4 changes: 2 additions & 2 deletions app/services/search/query_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class Grammar < Parslet::Parser
str("contributors") | str("participants") | str("messages") |
str("unread") | str("reading") | str("read") | str("new") |
str("starred") | str("notes") | str("tag") |
str("has")
str("has") | str("commitfest")
).as(:selector_key)
end

Expand All @@ -60,7 +60,7 @@ class Grammar < Parslet::Parser
str("first_after") | str("first_before") |
str("added_after") | str("added_before") |
str("messages") | str("count") | str("from") |
str("body") | str("name")
str("body") | str("name") | str("status") | str("tag")
).as(:condition_key)
end

Expand Down
22 changes: 17 additions & 5 deletions app/services/search/query_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,13 @@ class QueryValidator

TAG_SELECTORS = %i[tag].freeze

COMMITFEST_SELECTORS = %i[commitfest]

ALL_SELECTORS = (DATE_SELECTORS + COUNT_SELECTORS + AUTHOR_SELECTORS +
STATE_SELECTORS + CONTENT_SELECTORS + TAG_SELECTORS + [ :has ]).freeze
STATE_SELECTORS + CONTENT_SELECTORS + TAG_SELECTORS +
COMMITFEST_SELECTORS + [ :has ]).freeze

HAS_VALUES = %w[attachment patch contributor committer core_team].freeze
HAS_VALUES = %w[attachment patch contributor committer core_team commitfest].freeze

# Valid sub-conditions for each parent selector
VALID_SUB_CONDITIONS = {
Expand All @@ -34,7 +37,8 @@ class QueryValidator
attachment: %i[from count name],
patch: %i[from count]
},
tag: %i[from added_before added_after]
tag: %i[from added_before added_after],
commitfest: %i[name status tag]
}.freeze

# Sub-condition keywords that require date values
Expand Down Expand Up @@ -104,6 +108,8 @@ def validate_selector(node)
validate_content_selector(node)
when *TAG_SELECTORS
validate_tag_selector(node)
when *COMMITFEST_SELECTORS
validate_commitfest_selector(node)
when :has
validate_has_selector(node)
else
Expand All @@ -123,8 +129,8 @@ def validate_selector(node)
end

def supports_empty_value_with_conditions?(key, conditions)
# tag: can have empty value with conditions (e.g., tag:[from:me])
key == :tag && conditions.present?
# tag: and commitfest: can have empty value with conditions (e.g., tag:[from:me], commitfest:[tag:bugfix])
key.in?([ :tag, :commitfest ]) && conditions.present?
end

def validate_conditions(parent_key, parent_value, conditions)
Expand Down Expand Up @@ -182,6 +188,8 @@ def get_valid_sub_conditions(parent_key, parent_value)
has_conditions[normalized_value] || []
when :tag
VALID_SUB_CONDITIONS[:tag] || []
when :commitfest
VALID_SUB_CONDITIONS[:commitfest] || []
else
[]
end
Expand Down Expand Up @@ -252,6 +260,10 @@ def validate_tag_selector(node)
node
end

def validate_commitfest_selector(node)
node
end

def validate_has_selector(node)
value = node[:value].to_s.downcase

Expand Down
13 changes: 13 additions & 0 deletions app/views/help/pages/search.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,19 @@ last_after:1w # Topics with activity in the last week
messages_before:yesterday # Messages sent before yesterday
```

### Commitfest Selectors

Search for topics by commitfest.


| Selector | Description | Example |
|----------|-------------|---------|
| `commitfest:name` | Topics from commitfest with this name | `commitfest:PG19-Final` |
| `commitfest:[name:name]` | Topics from commitfest with this name | `commitfest:[name:PG19-Draft]` |
| `commitfest:[status:status]` | Topics from commitfest with this status | `commitfest:[status:commited]` |
| `commitfest:[tag:tag]` | Topics from commitfest with this tag | `commitfest:[tag:bugfix]` |


### Count Selectors

| Selector | Description | Example |
Expand Down
8 changes: 8 additions & 0 deletions spec/factories/commitfest_patch_commitfests.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
FactoryBot.define do
factory :commitfest_patch_commitfest do
commitfest
commitfest_patch

status { "Open" }
end
end
4 changes: 4 additions & 0 deletions spec/factories/commitfest_patch_tags.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
FactoryBot.define do
factory :commitfest_patch_tag do
end
end
6 changes: 6 additions & 0 deletions spec/factories/commitfest_patch_topics.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
FactoryBot.define do
factory :commitfest_patch_topic do
commitfest_patch
topic
end
end
27 changes: 27 additions & 0 deletions spec/factories/commitfest_patches.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
FactoryBot.define do
factory :commitfest_patch do
sequence(:external_id)
sequence(:title) { |n| "Patch Title ##{n}" }

trait :with_topic do
transient { topic { create(:topic) } }
after(:create) do |cp, ctx|
create(:commitfest_patch_topic, commitfest_patch: cp, topic: ctx.topic)
end
end

trait :with_commitfest do
transient { commitfest { create(:commitfest) } }
after(:create) do |cp, ctx|
create(:commitfest_patch_commitfest, commitfest_patch: cp, commitfest: ctx.commitfest)
end
end

trait :with_tag do
transient { commitfest_tag { create(:commitfest_tag) } }
after(:create) do |cp, ctx|
create(:commitfest_patch_tag, commitfest_patch: cp, commitfest_tag: ctx.commitfest_tag)
end
end
end
end
5 changes: 5 additions & 0 deletions spec/factories/commitfest_tags.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
FactoryBot.define do
factory :commitfest_tag do
name { "Bugfix" }
end
end
9 changes: 9 additions & 0 deletions spec/factories/commitfests.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
FactoryBot.define do
factory :commitfest do
sequence(:external_id)
sequence(:name) { |n| "PG#{n}-Final" }
status { "Open" }
start_date { 1.month.ago }
end_date { 1.month.from_now }
end
end
39 changes: 39 additions & 0 deletions spec/services/search/query_builder_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,45 @@ def build_query(query_string)
end
end
end

describe 'commitfest: selector' do
let!(:tag) { create(:commitfest_tag) }
let!(:commitfest_first) { create(:commitfest, name: 'PGX-Final') }
let!(:commitfest_second) { create(:commitfest) }
let!(:topic_first) { create(:topic) }
let!(:topic_second) { create(:topic) }

let!(:patch_first) { create(:commitfest_patch, :with_topic, :with_commitfest, :with_tag, topic: topic_first, commitfest: commitfest_first, commitfest_tag: tag) }
let!(:patch_second) { create(:commitfest_patch, :with_topic, topic: topic_second) }

before do
create(:commitfest_patch_commitfest, commitfest: commitfest_second, commitfest_patch: patch_second, status: 'Commited')
end

it 'filters topics with specific commitfest name' do
result = build_query('commitfest:PGX-Final')
expect(result.relation).to include(topic_first)
expect(result.relation).not_to include(topic_second)
end

it 'filters topics with specific commitfest name from condition' do
result = build_query('commitfest:[name:PGX-Final]')
expect(result.relation).to include(topic_first)
expect(result.relation).not_to include(topic_second)
end

it 'filters topics with specific commitfest status' do
result = build_query('commitfest:[status:commited]')
expect(result.relation).to include(topic_second)
expect(result.relation).not_to include(topic_first)
end

it 'filters topics with specific commitfest tag' do
result = build_query('commitfest:[tag:bugfix]')
expect(result.relation).to include(topic_first)
expect(result.relation).not_to include(topic_second)
end
end
end

describe 'negation' do
Expand Down
25 changes: 25 additions & 0 deletions spec/services/search/query_parser_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,31 @@
expect(result[:value]).to eq('')
expect(result[:conditions].size).to eq(1)
end

it 'parses commitfest: selector' do
result = parser.parse('commitfest:PG19-Final')
expect(result[:type]).to eq(:selector)
expect(result[:key]).to eq(:commitfest)
expect(result[:value]).to eq('PG19-Final')
end

it 'parses commitfest: selector with status: condition' do
result = parser.parse('commitfest:PG19-Draft[status:commited]')
expect(result[:type]).to eq(:selector)
expect(result[:key]).to eq(:commitfest)
expect(result[:value]).to eq('PG19-Draft')
expect(result[:conditions].size).to eq(1)
expect(result[:conditions][0][:key]).to eq(:status)
expect(result[:conditions][0][:value]).to eq('commited')
end

it 'parses commitfest: selector with empty value and from: condition' do
result = parser.parse('commitfest:[tag:bugfix]')
expect(result[:type]).to eq(:selector)
expect(result[:key]).to eq(:commitfest)
expect(result[:value]).to eq('')
expect(result[:conditions].size).to eq(1)
end
end

context 'with negation' do
Expand Down
36 changes: 36 additions & 0 deletions spec/services/search/query_validator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,42 @@
end
end

context 'with valid commitfest: selector' do
it 'passes valid commitfest name' do
ast = parser.parse('commitfest:PG19-Final')
result = described_class.new(ast).validate
expect(result.warnings).to be_empty
expect(result.ast[:key]).to eq(:commitfest)
end

it 'passes commitfest name with status: condition' do
ast = parser.parse('commitfest:PG19-Final[status:commited]')
result = described_class.new(ast).validate
expect(result.warnings).to be_empty
expect(result.ast[:conditions].size).to eq(1)
end

it 'passes commitfest name with tag: condition' do
ast = parser.parse('commitfest:PG19-Final[tag:commited]')
result = described_class.new(ast).validate
expect(result.warnings).to be_empty
expect(result.ast[:conditions].size).to eq(1)
end

it 'passes commitfest with multiple conditions' do
ast = parser.parse('commitfest:PG19-Draft[status:in_progress, tag:bugfix]')
result = described_class.new(ast).validate
expect(result.warnings).to be_empty
expect(result.ast[:conditions].size).to eq(2)
end

it 'warns on invalid condition for commitfest selector' do
ast = parser.parse('commitfest:[messages:>=10]')
result = described_class.new(ast).validate
expect(result.warnings).to include(/not valid for 'commitfest:'/i)
end
end

context 'with compound expressions' do
it 'validates children of AND expressions' do
ast = parser.parse('from:john first_after:invalid')
Expand Down