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
14 changes: 12 additions & 2 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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...
Expand All @@ -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.
Expand Down Expand Up @@ -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)
Expand Down
41 changes: 41 additions & 0 deletions examples/tag-removal-example.yml
Original file line number Diff line number Diff line change
@@ -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]
40 changes: 26 additions & 14 deletions lib/issuer/issue.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
#
Expand Down Expand Up @@ -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<String>] Tags to always append from CLI
# @param cli_default_tags [Array<String>] 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

Expand All @@ -199,21 +204,27 @@ 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
final_tags.concat(issue_regular_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
Expand Down Expand Up @@ -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<Array<String>>] Two-element array: [append_tags, default_tags]
Expand Down
72 changes: 72 additions & 0 deletions specs/tests/rspec/issue_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down