diff --git a/README.adoc b/README.adoc index 1a9121f..798ae59 100644 --- a/README.adoc +++ b/README.adoc @@ -144,7 +144,7 @@ $meta: # optional block for establishing general modes/settings vrsn: 0.1.0 # milestone/version user: alice # assigned user type: Bug # type of issue (must already be registered) - tags: [needs_labels,+posted_by_issuer] # labels + tags: [needs:labels, +posted_by_issuer, +needs:docs] # labels stub: true # whether to auto-insert stub texts head: | # header stub text to prepend when indicated Below the next line is the body... @@ -169,7 +169,9 @@ issues: # block for listing issues to post to cloud # --- # This is the default text that will appear if an issue record is a _stub_ and no `body` field is designated. # --- - # This issue was automatically generated by issuer. + # This issue was automatically generated by issuer. + - summ: Documentaton issue + tags: [-needs:docs] # skip the default needs:docs label ---- The `$meta` block is entirely optional, but if it is absent, your `issuer` command will need a `--proj` flag to designate the GitHub repo to which your issues should post. @@ -277,6 +279,14 @@ Defaults to `$meta.defaults.type` or else `null`. `tags`::: (Array of Strings) A listing of specific labels to assign to the issue. ++ +Supports special prefix notation for label management: ++ +* Regular labels (e.g., `bug`, `priority:high`) are applied based on default tag logic +* Append labels (e.g., `+urgent`) are always applied to all issues +* Removal labels (e.g., `-needs:docs`) remove the specified label from the default/appended labels list ++ +Example: `tags: [documentation, +critical, -needs:review]` would add `documentation` and `critical` labels while removing any `needs:review` label from defaults. `user`::: (String) diff --git a/examples/tag-removal-example.yml b/examples/tag-removal-example.yml new file mode 100644 index 0000000..0c8938e --- /dev/null +++ b/examples/tag-removal-example.yml @@ -0,0 +1,41 @@ +# Tag Removal Example - Issue #4 Implementation +# +# This example demonstrates the new tag removal functionality where +# tags prefixed with '-' are removed from the default/appended tags list. + +$meta: + proj: myorg/myrepo + defaults: + tags: [+posted-by-issuer, needs:label, +needs:docs] + +issues: + - summ: Fix misstatement in the README + body: | + Correct that really bad typo on the first paragraph. + No docs needed for this change since it's a docs change. + tags: [documentation, -needs:docs] + + - summ: Issue that will be fixed immediately + tags: + - '-needs:label' + + - summ: Complex issue with multiple tag operations + body: | + This issue demonstrates multiple tag operations: + - Adds bug and enhancement tags + - Removes the default needs:label tag + - Removes the appended needs:docs tag + - Keeps the posted-by-issuer tag (append) + tags: [bug, enhancement, -needs:label, -needs:docs] + + - summ: Issue with no custom tags + body: | + This issue has no custom tags, so it gets all default tags + including both regular defaults and append tags. + # No tags specified - gets all defaults + + - summ: Issue that only removes tags + body: | + This issue only removes tags, so it gets default tags + minus the removed ones. + tags: [-needs:docs] diff --git a/lib/issuer/issue.rb b/lib/issuer/issue.rb index 59aa43d..7b5831a 100644 --- a/lib/issuer/issue.rb +++ b/lib/issuer/issue.rb @@ -23,6 +23,7 @@ module Issuer # Tags support special prefix notation: # * Regular tags (e.g., +"bug"+) are only applied if the issue has no existing tags # * Append tags (e.g., +"+urgent"+) are always applied to all issues + # * Removal tags (e.g., +"-needs:docs"+) are removed from the default/appended tags list # # == Stub Composition # @@ -169,28 +170,32 @@ def self.apply_stub_logic issues, defaults # Apply tag logic for this issue # - # Processes existing tags with + prefix as append tags, combines them with - # CLI-provided tags, and determines final tag set based on precedence rules. + # Processes existing tags with + prefix as append tags, - prefix as removal tags, + # combines them with CLI-provided tags, and determines final tag set based on precedence rules. # # @param cli_append_tags [Array] Tags to always append from CLI # @param cli_default_tags [Array] Default tags from CLI (used when no regular tags exist) # @return [void] Sets @tags instance variable # # @example - # # Issue has tags: ['+urgent', 'bug'] - # issue.apply_tag_logic(['cli-tag'], ['default-tag']) - # # Result: ['urgent', 'cli-tag', 'bug'] + # # Issue has tags: ['+urgent', 'bug', '-needs:docs'] + # issue.apply_tag_logic(['cli-tag'], ['default-tag', 'needs:docs']) + # # Result: ['urgent', 'cli-tag', 'bug', 'default-tag'] (needs:docs removed) def apply_tag_logic cli_append_tags, cli_default_tags - # Parse existing tags for + prefix + # Parse existing tags for + and - prefixes existing_tags = tags || [] append_tags = [] regular_tags = [] + remove_tags = [] existing_tags.each do |tag| - if tag.to_s.start_with?('+') - append_tags << tag.to_s[1..] # Remove + prefix + tag_str = tag.to_s + if tag_str.start_with?('+') + append_tags << tag_str[1..] # Remove + prefix + elsif tag_str.start_with?('-') + remove_tags << tag_str[1..] # Remove - prefix else - regular_tags << tag.to_s + regular_tags << tag_str end end @@ -199,7 +204,7 @@ def apply_tag_logic cli_append_tags, cli_default_tags final_tags = append_tags + defaults_append_tags + cli_append_tags # For regular tags, add issue's own tags, otherwise use default tags - issue_regular_tags = Array(@raw_data['tags']).reject { |tag| tag.to_s.start_with?('+') } + issue_regular_tags = Array(@raw_data['tags']).reject { |tag| tag.to_s.start_with?('+') || tag.to_s.start_with?('-') } if !issue_regular_tags.empty? # Issue has its own regular tags, use them @@ -207,13 +212,19 @@ def apply_tag_logic cli_append_tags, cli_default_tags else # Issue has no regular tags, use defaults from CLI final_tags.concat(cli_default_tags) - # Also add non-append defaults tags + # Also add non-append defaults tags (- prefix ignored in defaults) defaults_regular_tags = Array(@defaults['tags']).reject { |tag| tag.to_s.start_with?('+') } final_tags.concat(defaults_regular_tags) end - # Set the final tags (removing duplicates) - @tags = final_tags.uniq + # Collect removal tags from issue only (not defaults) + all_remove_tags = remove_tags + + # Remove duplicates first, then remove tags specified for removal + final_tags = final_tags.uniq - all_remove_tags + + # Set the final tags + @tags = final_tags end # Apply stub logic for this issue @@ -259,7 +270,8 @@ def apply_stub_logic defaults # # Separates tags with + prefix (append tags) from regular tags (default tags). # Tags with + prefix are always applied, while regular tags are only used - # when the issue has no existing regular tags. + # when the issue has no existing regular tags. Tags with - prefix are handled + # in the apply_tag_logic method for removal. # # @param tags_string [String] Comma-separated tag string # @return [Array>] Two-element array: [append_tags, default_tags] diff --git a/specs/tests/rspec/issue_spec.rb b/specs/tests/rspec/issue_spec.rb index b3177cc..8d68bb5 100644 --- a/specs/tests/rspec/issue_spec.rb +++ b/specs/tests/rspec/issue_spec.rb @@ -349,6 +349,78 @@ expect(issue.tags).to contain_exactly('posted-by-issuer', 'cli-default', 'needs-label') end end + + context 'with removal tags (-prefix)' do + it 'removes tags specified with - prefix from issue tags' do + issue_data = { 'summ' => 'Test', 'tags' => ['bug', '-needs-label'] } + issue = described_class.new(issue_data, defaults) + + issue.apply_tag_logic([], []) + + expect(issue.tags).to contain_exactly('posted-by-issuer', 'bug') + expect(issue.tags).not_to include('needs-label') + end + + it 'removes append tags specified with - prefix' do + issue_data = { 'summ' => 'Test', 'tags' => ['bug', '-posted-by-issuer'] } + issue = described_class.new(issue_data, defaults) + + issue.apply_tag_logic([], []) + + expect(issue.tags).to contain_exactly('bug') + expect(issue.tags).not_to include('posted-by-issuer') + end + + it 'handles multiple removal tags' do + issue_data = { 'summ' => 'Test', 'tags' => ['bug', 'enhancement', '-needs-label', '-posted-by-issuer'] } + issue = described_class.new(issue_data, defaults) + + issue.apply_tag_logic([], []) + + expect(issue.tags).to contain_exactly('bug', 'enhancement') + expect(issue.tags).not_to include('needs-label', 'posted-by-issuer') + end + + it 'handles removal tags when no matching tags exist' do + issue_data = { 'summ' => 'Test', 'tags' => ['bug', '-nonexistent-tag'] } + issue = described_class.new(issue_data, defaults) + + issue.apply_tag_logic([], []) + + expect(issue.tags).to contain_exactly('posted-by-issuer', 'bug') + end + + it 'processes removal tags with CLI tags' do + issue_data = { 'summ' => 'Test', 'tags' => ['bug', '-needs-label'] } + issue = described_class.new(issue_data, defaults) + + issue.apply_tag_logic(['cli-urgent'], ['cli-default']) + + expect(issue.tags).to contain_exactly('posted-by-issuer', 'cli-urgent', 'bug') + expect(issue.tags).not_to include('needs-label') + end + + it 'handles colon-separated tags in removal (needs:docs example)' do + defaults_with_colon = { 'tags' => ['+posted-by-issuer', 'needs:docs', 'needs:label'] } + issue_data = { 'summ' => 'Test', 'tags' => ['documentation', '-needs:docs'] } + issue = described_class.new(issue_data, defaults_with_colon) + + issue.apply_tag_logic([], []) + + expect(issue.tags).to contain_exactly('posted-by-issuer', 'documentation') + expect(issue.tags).not_to include('needs:docs') + end + + it 'handles string format removal tags' do + issue_data = { 'summ' => 'Test', 'tags' => ['-needs-label'] } + issue = described_class.new(issue_data, defaults) + + issue.apply_tag_logic([], []) + + expect(issue.tags).to contain_exactly('posted-by-issuer') + expect(issue.tags).not_to include('needs-label') + end + end end describe '.parse_tag_logic' do