torque 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +3 -0
- data/LICENSE +18 -0
- data/README.md +116 -0
- data/VERSION +3 -0
- data/bin/config +257 -0
- data/bin/email +172 -0
- data/bin/project +136 -0
- data/bin/torque +150 -0
- data/lib/torque.rb +190 -0
- data/lib/torque/date_settings.rb +166 -0
- data/lib/torque/error/invalid_project_error.rb +10 -0
- data/lib/torque/error/invalid_token_error.rb +10 -0
- data/lib/torque/error/missing_output_directory_error.rb +7 -0
- data/lib/torque/error/missing_project_error.rb +8 -0
- data/lib/torque/error/missing_token_error.rb +8 -0
- data/lib/torque/error/missing_torque_info_file_error.rb +8 -0
- data/lib/torque/error/pivotal_api_error.rb +8 -0
- data/lib/torque/file_system.rb +70 -0
- data/lib/torque/mailer.rb +62 -0
- data/lib/torque/pivotal.rb +98 -0
- data/lib/torque/pivotal_html_parser.rb +67 -0
- data/lib/torque/project/project.rb +25 -0
- data/lib/torque/project/project_manager.rb +140 -0
- data/lib/torque/record_pathname_settings.rb +80 -0
- data/lib/torque/settings.rb +164 -0
- data/lib/torque/story.rb +108 -0
- data/lib/torque/torque_info_parser.rb +230 -0
- data/lib/torque/version.rb +24 -0
- metadata +145 -0
@@ -0,0 +1,67 @@
|
|
1
|
+
# Processes a project by matching commits in its git commit history to pivotal id's
|
2
|
+
# Should have already run the getProject script, generating "commitHistory.txt" and "project_html.txt"
|
3
|
+
|
4
|
+
require 'date'
|
5
|
+
require 'nokogiri'
|
6
|
+
require_relative 'story'
|
7
|
+
|
8
|
+
class Torque
|
9
|
+
class PivotalHTMLParser
|
10
|
+
|
11
|
+
##
|
12
|
+
# @param project_html_string An html string containing the story data for a Pivotal Tracker project
|
13
|
+
# @param accept_from A Date marking the lower bound for the date_accepted field of a story
|
14
|
+
# @param accept_to A Date marking the upper bound for the date_accepted field of a story
|
15
|
+
#
|
16
|
+
# Returns a list of Story objects parsed from project_html_string whose date_accepted fields are within acceptable
|
17
|
+
# bounds (least recent to most recent date accepted)
|
18
|
+
def process_project_date_filter(project_html_string, accept_from, accept_to)
|
19
|
+
|
20
|
+
story_list = process_project(project_html_string)
|
21
|
+
story_list.select! {
|
22
|
+
|story|
|
23
|
+
(!story.date_accepted.nil? \
|
24
|
+
&& story.date_accepted >= accept_from \
|
25
|
+
&& story.date_accepted <= accept_to)
|
26
|
+
}
|
27
|
+
|
28
|
+
story_list
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
# @param project_html_string An html string containing the story data for a Pivotal Tracker project
|
33
|
+
#
|
34
|
+
# Returns a list of all Story objects parsed from project_html_string (least recent to most recent date accepted)
|
35
|
+
def process_project(project_html_string)
|
36
|
+
|
37
|
+
project_html = Nokogiri::HTML(project_html_string)
|
38
|
+
story_html_array = project_html.search('story')
|
39
|
+
story_list = []
|
40
|
+
|
41
|
+
story_html_array.each do
|
42
|
+
|story_element|
|
43
|
+
story_list << process_story(story_element)
|
44
|
+
end
|
45
|
+
|
46
|
+
story_list
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
private
|
51
|
+
# story: A Nokogiri::XML::Element
|
52
|
+
#
|
53
|
+
# Returns a Story object based off the information in the story
|
54
|
+
def process_story(story_element)
|
55
|
+
|
56
|
+
story_hash = {}
|
57
|
+
|
58
|
+
story_element.children.each do
|
59
|
+
|child|
|
60
|
+
story_hash[child.name] = child.text
|
61
|
+
end
|
62
|
+
|
63
|
+
Story.create(story_hash)
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class Torque
|
2
|
+
|
3
|
+
##
|
4
|
+
# Stores the name and id of a single Pivotal project
|
5
|
+
class Project
|
6
|
+
|
7
|
+
##
|
8
|
+
# True if this is the current project, else false
|
9
|
+
attr_accessor :current
|
10
|
+
|
11
|
+
##
|
12
|
+
# The project ID
|
13
|
+
attr_reader :id
|
14
|
+
|
15
|
+
##
|
16
|
+
# The project name
|
17
|
+
attr_reader :name
|
18
|
+
|
19
|
+
def initialize(id, name, current=false)
|
20
|
+
@current = current
|
21
|
+
@id = id
|
22
|
+
@name = name
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
lib_dir = File.expand_path(File.dirname(__FILE__)) + "/../.."
|
2
|
+
|
3
|
+
require 'net/http'
|
4
|
+
require 'nokogiri'
|
5
|
+
require 'pathname'
|
6
|
+
require 'yaml'
|
7
|
+
|
8
|
+
require_relative "#{lib_dir}/torque/file_system"
|
9
|
+
require_relative "#{lib_dir}/torque/pivotal"
|
10
|
+
require_relative "#{lib_dir}/torque/torque_info_parser"
|
11
|
+
require_relative "#{lib_dir}/torque/error/invalid_token_error"
|
12
|
+
require_relative "#{lib_dir}/torque/error/missing_token_error"
|
13
|
+
require_relative "#{lib_dir}/torque/error/pivotal_api_error"
|
14
|
+
require_relative "project"
|
15
|
+
|
16
|
+
class Torque
|
17
|
+
|
18
|
+
##
|
19
|
+
# Retrieves the list of Pivotal Tracker projects from the Pivotal API, automates the switching of projects
|
20
|
+
class ProjectManager
|
21
|
+
|
22
|
+
##
|
23
|
+
# @param torque_info_parser An instance of the TorqueInfoParser class
|
24
|
+
def initialize(torque_info_parser=TorqueInfoParser.new)
|
25
|
+
@torque_info_parser = torque_info_parser
|
26
|
+
end
|
27
|
+
|
28
|
+
##
|
29
|
+
# @param token A Pivotal Tracker API token (default: Loads token from .torqueinfo.yaml)
|
30
|
+
#
|
31
|
+
# Requests and processes the project list from the Pivotal Tracker API
|
32
|
+
#
|
33
|
+
# Returns the project list
|
34
|
+
def load_project_list(token=nil)
|
35
|
+
get_project_list(token)
|
36
|
+
@project_list
|
37
|
+
end
|
38
|
+
|
39
|
+
##
|
40
|
+
# Returns the project list if 'load_project_list' has been called, else returns nil
|
41
|
+
#
|
42
|
+
# Does not do any processing
|
43
|
+
def project_list
|
44
|
+
@project_list
|
45
|
+
end
|
46
|
+
|
47
|
+
##
|
48
|
+
# Returns the current project if 'load_project_list' has been called and one exists, else returns nil
|
49
|
+
def current_project
|
50
|
+
@project_list.nil? \
|
51
|
+
? nil
|
52
|
+
: @project_list.select{|project| project.current}[0]
|
53
|
+
end
|
54
|
+
|
55
|
+
##
|
56
|
+
# Formats the project list as a printable string
|
57
|
+
#
|
58
|
+
# Prepends an asterisk to the current project
|
59
|
+
def format_project_list
|
60
|
+
|
61
|
+
list_str = "PROJECTS\n"
|
62
|
+
@project_list.each do |project|
|
63
|
+
|
64
|
+
project.current \
|
65
|
+
? list_str += "* " \
|
66
|
+
: list_str += " "
|
67
|
+
list_str += "#{project.id} #{project.name}"
|
68
|
+
list_str += "\n"
|
69
|
+
end
|
70
|
+
|
71
|
+
list_str
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
# token: The API token to use for the request
|
78
|
+
#
|
79
|
+
# Retrieves the project list from Pivotal Tracker
|
80
|
+
# Stores the project list in @project_list
|
81
|
+
# Returns the project list
|
82
|
+
def get_project_list(token=nil)
|
83
|
+
|
84
|
+
# Parses data from the torque info file
|
85
|
+
torque_info_token, project_id = parse_torque_info
|
86
|
+
token = torque_info_token unless token
|
87
|
+
|
88
|
+
# If the API token doesn't exist, prompt for it
|
89
|
+
raise MissingTokenError.new "API token for Pivotal Tracker has not been set" \
|
90
|
+
unless token
|
91
|
+
|
92
|
+
# Parses the project list from the Pivotal Tracker API
|
93
|
+
html = Pivotal.new(token).get_project_data
|
94
|
+
@project_list = parse_project_html(html)
|
95
|
+
|
96
|
+
identify_current_project(project_id)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Identifies the current project and marks it as such
|
100
|
+
def identify_current_project(project_id)
|
101
|
+
@project_list.each do
|
102
|
+
|project|
|
103
|
+
if String(project_id) == project.id
|
104
|
+
project.current = true
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# Parses the API token and current project from .torqueinfo.yaml
|
110
|
+
# Returns [token, project]
|
111
|
+
def parse_torque_info
|
112
|
+
|
113
|
+
torque_info = @torque_info_parser.parse
|
114
|
+
|
115
|
+
return torque_info.token, torque_info.project
|
116
|
+
end
|
117
|
+
|
118
|
+
# Parses the html returned from the Pivotal API
|
119
|
+
def parse_project_html(html)
|
120
|
+
project_html = Nokogiri::HTML(html)
|
121
|
+
project_html_array = project_html.search('project')
|
122
|
+
project_list = []
|
123
|
+
|
124
|
+
project_html_array.each do
|
125
|
+
|project_element|
|
126
|
+
|
127
|
+
project_hash = {}
|
128
|
+
project_element.children.each do
|
129
|
+
|child|
|
130
|
+
project_hash[child.name] = child.text
|
131
|
+
end
|
132
|
+
|
133
|
+
project_list << Project.new(project_hash["id"], project_hash["name"])
|
134
|
+
end
|
135
|
+
|
136
|
+
project_list
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require_relative 'file_system'
|
2
|
+
|
3
|
+
class Torque
|
4
|
+
|
5
|
+
##
|
6
|
+
# Generates the pathname to use for the record file of the release notes
|
7
|
+
class RecordPathnameSettings
|
8
|
+
|
9
|
+
##
|
10
|
+
# @param output_dir The path to the release notes output directory
|
11
|
+
# @param project The project id
|
12
|
+
# @param custom_date_range True if a custom date range is being used, else false
|
13
|
+
# @param fs An instance of the FileSystem class
|
14
|
+
def initialize(output_dir, project, custom_date_range, fs)
|
15
|
+
@output_dir = output_dir
|
16
|
+
@project = project
|
17
|
+
@custom = custom_date_range
|
18
|
+
@fs = fs
|
19
|
+
end
|
20
|
+
|
21
|
+
##
|
22
|
+
# Returns the path to the record file, generating one if it does not exist
|
23
|
+
def get_path
|
24
|
+
generate_record_path if !@path
|
25
|
+
@path
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
# Generates a value for @path
|
32
|
+
def generate_record_path
|
33
|
+
if @custom
|
34
|
+
path_for_custom_date_range
|
35
|
+
else
|
36
|
+
path_for_default_date_range
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# The path to the record of the release notes file if default dates were used
|
41
|
+
def path_for_default_date_range
|
42
|
+
title = "#{@project}-#{current_date_string}"
|
43
|
+
@path="#{@output_dir}/previous/#{title}.txt"
|
44
|
+
@path
|
45
|
+
end
|
46
|
+
|
47
|
+
# The path to the record of the release notes file if a custom date range was used
|
48
|
+
def path_for_custom_date_range
|
49
|
+
title = "#{@project}-#{current_date_string}"
|
50
|
+
title += "-custom"
|
51
|
+
path_base = "#{@output_dir}/previous/#{title}"
|
52
|
+
path_to_test = "#{path_base}.txt"
|
53
|
+
|
54
|
+
# If the first pathname tried is not in use, use it
|
55
|
+
if !(@fs.path_exist? path_to_test)
|
56
|
+
@path=path_to_test
|
57
|
+
|
58
|
+
# Else, will append "1", "2", "3"... to the end of the pathname, returning the first name that is not in use
|
59
|
+
else
|
60
|
+
i=1
|
61
|
+
while true
|
62
|
+
path_to_test = "#{path_base}#{i}.txt"
|
63
|
+
if !(@fs.path_exist? path_to_test)
|
64
|
+
@path=path_to_test
|
65
|
+
break
|
66
|
+
end
|
67
|
+
i+=1
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
@path
|
72
|
+
end
|
73
|
+
|
74
|
+
# A string representing the current date: YYYY-MM-DD
|
75
|
+
def current_date_string
|
76
|
+
Date.today.strftime("%Y-%m-%d")
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
require 'date'
|
2
|
+
require_relative 'date_settings'
|
3
|
+
require_relative 'file_system'
|
4
|
+
require_relative 'record_pathname_settings'
|
5
|
+
require_relative 'torque_info_parser'
|
6
|
+
require_relative 'error/missing_project_error'
|
7
|
+
require_relative 'error/missing_token_error'
|
8
|
+
require_relative 'error/missing_output_directory_error'
|
9
|
+
|
10
|
+
class Torque
|
11
|
+
|
12
|
+
##
|
13
|
+
# Stores all of the settings for a release notes generation
|
14
|
+
class Settings
|
15
|
+
|
16
|
+
##
|
17
|
+
# The accept-from date. All stories accepted on Pivotal Tracker before this date will be ignored
|
18
|
+
attr_reader :accept_from
|
19
|
+
|
20
|
+
##
|
21
|
+
# The accept-to date. All stories accepted on Pivotal Tracker after this date will be ignored
|
22
|
+
attr_reader :accept_to
|
23
|
+
|
24
|
+
##
|
25
|
+
# The path to the most recently generated notes
|
26
|
+
attr_reader :current_notes_path
|
27
|
+
|
28
|
+
##
|
29
|
+
# True if a custom date range is being used (set manually through the command line), false if using the default one
|
30
|
+
attr_reader :custom_date_range
|
31
|
+
|
32
|
+
##
|
33
|
+
# True if should send the notes to the email list after notes are generated, else false
|
34
|
+
attr_reader :email
|
35
|
+
|
36
|
+
##
|
37
|
+
# The email address from which to send notes
|
38
|
+
attr_reader :email_address
|
39
|
+
|
40
|
+
##
|
41
|
+
# The password to the email address from which to send notes
|
42
|
+
attr_reader :email_password
|
43
|
+
|
44
|
+
##
|
45
|
+
# A list of email addresses to send the notes to
|
46
|
+
attr_reader :email_to
|
47
|
+
|
48
|
+
##
|
49
|
+
# The path to the .last-run file in the records directory
|
50
|
+
attr_reader :last_run_path
|
51
|
+
|
52
|
+
##
|
53
|
+
# The Pivotal Tracker project ID to use
|
54
|
+
attr_reader :project
|
55
|
+
|
56
|
+
##
|
57
|
+
# The output directory to use
|
58
|
+
attr_reader :output_dir
|
59
|
+
|
60
|
+
##
|
61
|
+
# The path to the record file of the notes
|
62
|
+
attr_reader :record_path
|
63
|
+
|
64
|
+
##
|
65
|
+
# The path to the root directory (the directory which contains a .torqueinfo.yaml file)
|
66
|
+
attr_reader :root_dir
|
67
|
+
|
68
|
+
##
|
69
|
+
# True if should silence all output, else false
|
70
|
+
attr_reader :silent
|
71
|
+
|
72
|
+
##
|
73
|
+
# The Pivotal Tracker api token to use to access the Pivotal project
|
74
|
+
attr_reader :token
|
75
|
+
|
76
|
+
##
|
77
|
+
# The path to the .torqueinfo.yaml file
|
78
|
+
attr_reader :torque_info_path
|
79
|
+
|
80
|
+
##
|
81
|
+
# True if should be verbose, else false
|
82
|
+
attr_reader :verbose
|
83
|
+
|
84
|
+
# @param options A hash of the options used to run the program
|
85
|
+
# @param fs An instance of the FileSystem class
|
86
|
+
#
|
87
|
+
# Determines the project settings from the environment
|
88
|
+
def initialize(options={}, fs=FileSystem.new)
|
89
|
+
|
90
|
+
@options = options
|
91
|
+
@fs = fs
|
92
|
+
|
93
|
+
# Handles basic "true/false" options
|
94
|
+
|
95
|
+
@email = @options[:email] || false
|
96
|
+
@silent = @options[:silent] || false
|
97
|
+
@verbose = @options[:verbose] || false
|
98
|
+
|
99
|
+
# Initializes the root directory for Torque
|
100
|
+
|
101
|
+
@root_dir = @options[:root_dir] || "."
|
102
|
+
@root_dir = File.expand_path(@root_dir)
|
103
|
+
|
104
|
+
# Parses and processes data from .torqueinfo.yaml
|
105
|
+
|
106
|
+
@torque_info_path = "#{@root_dir}/.torqueinfo.yaml"
|
107
|
+
torque_info = TorqueInfoParser.new(torque_info_path).parse
|
108
|
+
|
109
|
+
@email_address = torque_info.email_address
|
110
|
+
@email_password = torque_info.email_password
|
111
|
+
@email_to = torque_info.email_to
|
112
|
+
@output_dir = torque_info.output_dir
|
113
|
+
@project = torque_info.project
|
114
|
+
@token = torque_info.token
|
115
|
+
|
116
|
+
raise MissingTokenError.new(
|
117
|
+
"API token for Pivotal Tracker has not been set"
|
118
|
+
) if @token.blank?
|
119
|
+
raise MissingProjectError.new(
|
120
|
+
"Project ID for Pivotal Tracker has not been set"
|
121
|
+
) if @project.blank?
|
122
|
+
|
123
|
+
@output_dir = "release_notes" if @output_dir.blank?
|
124
|
+
@output_dir = "#{@root_dir}/#{@output_dir}"
|
125
|
+
|
126
|
+
if @email_to.class == NilClass; @email_to = []
|
127
|
+
elsif @email_to.class == String; @email_to = [@email_to]
|
128
|
+
elsif @email_to.class == Array; @email_to = @email_to
|
129
|
+
else; raise "Unknown parsing error on .torqueinfo.yaml's 'email_to' field: #{@email_to}"
|
130
|
+
end
|
131
|
+
|
132
|
+
# Sets up the output directory, throwing an error if it cannot be found
|
133
|
+
|
134
|
+
if !@fs.path_exist? @output_dir
|
135
|
+
raise MissingOutputDirectoryError.new(
|
136
|
+
"Could not find the output directory: #{@output_dir}"
|
137
|
+
)
|
138
|
+
elsif !@fs.path_exist? "#{@output_dir}/previous"
|
139
|
+
@fs.mkdir_p("#{@output_dir}/previous")
|
140
|
+
end
|
141
|
+
|
142
|
+
# The path to the last-run file for this project
|
143
|
+
|
144
|
+
@last_run_path = "#{output_dir}/previous/.last-run-#{project}"
|
145
|
+
|
146
|
+
# Determines the date range within which to accept stories
|
147
|
+
|
148
|
+
date_settings = DateSettings.new(@options, @last_run_path, @fs)
|
149
|
+
@accept_from, @accept_to = date_settings.get_dates
|
150
|
+
@custom_date_range = date_settings.custom_date_range?
|
151
|
+
|
152
|
+
# Sets the path to the main "release-notes.txt" file
|
153
|
+
|
154
|
+
@current_notes_path = "#{output_dir}/release-notes.txt"
|
155
|
+
|
156
|
+
# Determines the path name to use for the record of the output file
|
157
|
+
|
158
|
+
record_pathname_settings = RecordPathnameSettings.new(@output_dir, @project, @custom_date_range, @fs)
|
159
|
+
@record_path = record_pathname_settings.get_path
|
160
|
+
|
161
|
+
end
|
162
|
+
|
163
|
+
end
|
164
|
+
end
|