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
30 changes: 29 additions & 1 deletion lib/smart_todo/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,40 @@ def process_todos(todos)
end

@errors.concat(todo.errors)
dispatches << [event_message, todo] if event_met

next unless event_met

event_message = append_context_if_applicable(event_message, todo, event_met, events)

dispatches << [event_message, todo]
end

dispatches
end

private

# @param event_message [String] the original event message
# @param todo [Todo] the todo object that may contain context
# @param event [Event] the event that was met
# @param events [Events] the events instance for fetching issue context
# @return [String] the event message, potentially with context appended
def append_context_if_applicable(event_message, todo, event, events)
return event_message unless should_apply_context?(todo, event)

org, repo, issue_number = todo.context.arguments
context_message = events.issue_context(org, repo, issue_number)

context_message ? "#{event_message}\n\n#{context_message}" : event_message
end

# @param todo [Todo] the todo object to check for context
# @param event [Event] the event to check
# @return [Boolean] true if context should be applied, false otherwise
def should_apply_context?(todo, event)
!!todo.context
end

def process_dispatches(dispatches)
queue = Queue.new
dispatches.each { |dispatch| queue << dispatch }
Expand Down
26 changes: 26 additions & 0 deletions lib/smart_todo/events.rb
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,32 @@ def pull_request_close(organization, repo, pr_number)
end
end

# Retrieve context information for an issue
# This is used when a TODO has a context: issue() attribute
#
# @param organization [String] the GitHub organization name
# @param repo [String] the GitHub repo name
# @param issue_number [String, Integer]
# @return [String, nil]
def issue_context(organization, repo, issue_number)
headers = github_headers(organization, repo)
response = github_client.get("/repos/#{organization}/#{repo}/issues/#{issue_number}", headers)

if response.code_type < Net::HTTPClientError
nil
else
issue = JSON.parse(response.body)
state = issue["state"]
title = issue["title"]
assignee = issue["assignee"] ? "@#{issue["assignee"]["login"]}" : "unassigned"

"📌 Context: Issue ##{issue_number} - \"#{title}\" [#{state}] (#{assignee}) - " \
"https://github.com/#{organization}/#{repo}/issues/#{issue_number}"
end
rescue Net::HTTPError, JSON::ParserError
nil
end

# Check if the installed ruby version meets requirements.
#
# @param requirements [Array<String>] a list of version specifiers
Expand Down
24 changes: 24 additions & 0 deletions lib/smart_todo/todo.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ module SmartTodo
class Todo
attr_reader :filepath, :comment, :indent
attr_reader :events, :assignees, :errors
attr_accessor :context

def initialize(source, filepath = "-e")
@filepath = filepath
Expand All @@ -12,6 +13,7 @@ def initialize(source, filepath = "-e")

@events = []
@assignees = []
@context = nil
@errors = []

parse(source[(indent + 1)..])
Expand Down Expand Up @@ -66,6 +68,28 @@ def visit_keyword_hash_node(node)
end
when :to
metadata.assignees << visit(element.value)
when :context
value = visit(element.value)

unless value.is_a?(String)
metadata.errors << "Incorrect `:context` format: expected string value"
next
end

unless value =~ %r{^([^/]+)/([^#]+)#(\d+)$}
metadata.errors << "Incorrect `:context` format: expected \"org/repo#issue_number\""
next
end

org = ::Regexp.last_match(1)
repo = ::Regexp.last_match(2)
issue_number = ::Regexp.last_match(3)

if org.empty? || repo.empty?
metadata.errors << "Incorrect `:context` format: org and repo cannot be empty"
else
metadata.context = CallNode.new(:issue, [org, repo, issue_number], element.value.location)
end
end
end
end
Expand Down
36 changes: 36 additions & 0 deletions test/smart_todo/cli_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -150,5 +150,41 @@ def hello
end
end
end

def test_should_apply_context_returns_false_without_context
cli = CLI.new
todo = Todo.new("# TODO(on: date('2015-03-01'), to: '[email protected]')")
event = Todo::CallNode.new(:date, ["2015-03-01"], nil)

refute(cli.send(:should_apply_context?, todo, event))
end

def test_should_apply_context_returns_true_for_issue_close_event_with_context
cli = CLI.new
todo = Todo.new(
"# TODO(on: issue_close('org', 'repo', '123'), to: '[email protected]', context: \"org/repo#456\")",
)
event = Todo::CallNode.new(:issue_close, ["org", "repo", "123"], nil)

assert(cli.send(:should_apply_context?, todo, event))
end

def test_should_apply_context_returns_true_for_pull_request_close_event_with_context
cli = CLI.new
todo = Todo.new(
"# TODO(on: pull_request_close('org', 'repo', '123'), to: '[email protected]', context: \"org/repo#456\")",
)
event = Todo::CallNode.new(:pull_request_close, ["org", "repo", "123"], nil)

assert(cli.send(:should_apply_context?, todo, event))
end

def test_should_apply_context_returns_true_for_regular_event_with_context
cli = CLI.new
todo = Todo.new("# TODO(on: date('2015-03-01'), to: '[email protected]', context: \"org/repo#456\")")
event = Todo::CallNode.new(:date, ["2015-03-01"], nil)

assert(cli.send(:should_apply_context?, todo, event))
end
end
end
147 changes: 147 additions & 0 deletions test/smart_todo/events/issue_context_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# frozen_string_literal: true

require "test_helper"

module SmartTodo
class Events
class IssueContextTest < Minitest::Test
def test_when_issue_exists_and_is_open
stub_request(:get, /api.github.com/)
.to_return_json(body: {
state: "open",
title: "Add support for caching",
number: 123,
assignee: { login: "johndoe" },
})

expected = "📌 Context: Issue #123 - \"Add support for caching\" [open] (@johndoe) - " \
"https://github.com/rails/rails/issues/123"

assert_equal(expected, issue_context("rails", "rails", "123"))
end

def test_when_issue_exists_and_is_closed
stub_request(:get, /api.github.com/)
.to_return_json(body: {
state: "closed",
title: "Fix memory leak",
number: 456,
assignee: { login: "janedoe" },
})

expected = "📌 Context: Issue #456 - \"Fix memory leak\" [closed] (@janedoe) - " \
"https://github.com/shopify/smart_todo/issues/456"

assert_equal(expected, issue_context("shopify", "smart_todo", "456"))
end

def test_when_issue_has_no_assignee
stub_request(:get, /api.github.com/)
.to_return_json(body: {
state: "open",
title: "Improve documentation",
number: 789,
assignee: nil,
})

expected = "📌 Context: Issue #789 - \"Improve documentation\" [open] (unassigned) - " \
"https://github.com/org/repo/issues/789"

assert_equal(expected, issue_context("org", "repo", "789"))
end

def test_when_issue_does_not_exist
stub_request(:get, /api.github.com/)
.to_return(status: 404)

assert_nil(issue_context("rails", "rails", "999"))
end

def test_when_token_env_is_not_present
stub_request(:get, /api.github.com/)
.to_return_json(body: {
state: "open",
title: "Test issue",
number: 1,
assignee: nil,
})

result = issue_context("rails", "rails", "1")
assert(result.include?("📌 Context: Issue #1"))

assert_requested(:get, /api.github.com/) do |request|
assert(!request.headers.key?("Authorization"))
end
end

def test_when_token_env_is_present
ENV[GITHUB_TOKEN] = "abc123"

stub_request(:get, /api.github.com/)
.to_return_json(body: {
state: "open",
title: "Test issue",
number: 2,
assignee: nil,
})

result = issue_context("rails", "rails", "2")
assert(result.include?("📌 Context: Issue #2"))

assert_requested(:get, /api.github.com/) do |request|
assert_equal("token abc123", request.headers["Authorization"])
end
ensure
ENV.delete(GITHUB_TOKEN)
end

def test_when_organization_specific_token_is_present
ENV["#{GITHUB_TOKEN}__RAILS"] = "rails_token"

stub_request(:get, /api.github.com/)
.to_return_json(body: {
state: "open",
title: "Test issue",
number: 3,
assignee: nil,
})

result = issue_context("rails", "rails", "3")
assert(result.include?("📌 Context: Issue #3"))

assert_requested(:get, /api.github.com/) do |request|
assert_equal("token rails_token", request.headers["Authorization"])
end
ensure
ENV.delete("#{GITHUB_TOKEN}__RAILS")
end

def test_when_repo_specific_token_is_present
ENV["#{GITHUB_TOKEN}__RAILS__RAILS"] = "rails_rails_token"

stub_request(:get, /api.github.com/)
.to_return_json(body: {
state: "open",
title: "Test issue",
number: 4,
assignee: nil,
})

result = issue_context("rails", "rails", "4")
assert(result.include?("📌 Context: Issue #4"))

assert_requested(:get, /api.github.com/) do |request|
assert_equal("token rails_rails_token", request.headers["Authorization"])
end
ensure
ENV.delete("#{GITHUB_TOKEN}__RAILS__RAILS")
end

private

def issue_context(*args)
Events.new.issue_context(*args)
end
end
end
end
Loading