torque 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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,166 @@
|
|
1
|
+
require_relative 'file_system'
|
2
|
+
|
3
|
+
class Torque
|
4
|
+
|
5
|
+
##
|
6
|
+
# Determines the date range for a release notes generation
|
7
|
+
class DateSettings
|
8
|
+
|
9
|
+
##
|
10
|
+
# @param options A hash of the command line options used to run Torque
|
11
|
+
# @param last_run_path The path to the lastRun file
|
12
|
+
# @param fs An instance of the FileSystem class
|
13
|
+
def initialize(options, last_run_path, fs)
|
14
|
+
@options = options
|
15
|
+
@last_run_path = last_run_path
|
16
|
+
@fs = fs
|
17
|
+
|
18
|
+
@current_date = Date.today
|
19
|
+
@beginning_of_time = Date.new(1900, 1, 1)
|
20
|
+
@date_format = "%Y-%m-%d"
|
21
|
+
end
|
22
|
+
|
23
|
+
##
|
24
|
+
# Generates the accept_from and accept_to dates if they don't already exist
|
25
|
+
#
|
26
|
+
# Returns [accept_from, accept_to]
|
27
|
+
def get_dates
|
28
|
+
generate_dates if !@accept_from || !@accept_to
|
29
|
+
return @accept_from, @accept_to
|
30
|
+
end
|
31
|
+
|
32
|
+
##
|
33
|
+
# Returns true if the date range was set manually via command line, else false
|
34
|
+
def custom_date_range?
|
35
|
+
generate_dates if !@custom_date_range
|
36
|
+
return @custom_date_range
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
# Generates values for @accept_from and @accept_to
|
43
|
+
# Returns [@accept_from, @accept_to]
|
44
|
+
def generate_dates
|
45
|
+
|
46
|
+
# If custom date settings exist, takes dates from there
|
47
|
+
if @options[:accept_from] || @options[:accept_to]
|
48
|
+
get_dates_from_custom_range
|
49
|
+
@custom_date_range = true
|
50
|
+
|
51
|
+
# If not:
|
52
|
+
# @accept_to = today
|
53
|
+
# @accept_from is calculated from the dates of previous generations
|
54
|
+
else
|
55
|
+
get_dates_from_default
|
56
|
+
@custom_date_range = false
|
57
|
+
end
|
58
|
+
|
59
|
+
return @accept_from, @accept_to
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
# Handles date values that were set manually in the environment
|
64
|
+
# Returns [@accept_from, @accept_to]
|
65
|
+
def get_dates_from_custom_range
|
66
|
+
begin
|
67
|
+
@accept_from = Date.parse(@options[:accept_from].nil? ? "1900-01-01" : @options[:accept_from])
|
68
|
+
rescue ArgumentError
|
69
|
+
raise ArgumentError.new("Date #{@options[:accept_from]} could not be parsed")
|
70
|
+
end
|
71
|
+
|
72
|
+
begin
|
73
|
+
@accept_to = (@options[:accept_to].nil? ? @current_date : Date.parse(@options[:accept_to]))
|
74
|
+
rescue ArgumentError
|
75
|
+
raise ArgumentError.new("Date #{@options[:accept_to]} could not be parsed")
|
76
|
+
end
|
77
|
+
|
78
|
+
if @accept_from > @accept_to
|
79
|
+
raise ArgumentError.new("Date ACCEPT_FROM (#{@accept_from}) occurs after ACCEPT_TO (#{@accept_to})")
|
80
|
+
end
|
81
|
+
|
82
|
+
return @accept_from, @accept_to
|
83
|
+
end
|
84
|
+
|
85
|
+
# dates: An array of the dates contained by .lastRun
|
86
|
+
#
|
87
|
+
# Reads from the .last-run file for the project to figure out when notes were last generated
|
88
|
+
# Returns [@accept_from, @accept_to]
|
89
|
+
def get_dates_from_default
|
90
|
+
|
91
|
+
dates = update_last_run_file
|
92
|
+
|
93
|
+
@accept_to = @current_date
|
94
|
+
@accept_from = dates[1] || @beginning_of_time
|
95
|
+
|
96
|
+
return @accept_from, @accept_to
|
97
|
+
end
|
98
|
+
|
99
|
+
# Updates .last-run on the filesystem, returning the dates it contains after the update
|
100
|
+
def update_last_run_file
|
101
|
+
|
102
|
+
# Structure of last-run file:
|
103
|
+
#
|
104
|
+
# <Most recent generation date>
|
105
|
+
# <Second most recent generation date>
|
106
|
+
|
107
|
+
# Creates the .last-run if none exists
|
108
|
+
if !@fs.path_exist? @last_run_path
|
109
|
+
@fs.file_create(@last_run_path)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Generates and writes the new file contents
|
113
|
+
new_file_str, dates = update_last_run_file_string(@fs.file_read(@last_run_path))
|
114
|
+
@fs.file_write(@last_run_path, new_file_str)
|
115
|
+
|
116
|
+
# Returns the new dates
|
117
|
+
dates
|
118
|
+
|
119
|
+
end
|
120
|
+
|
121
|
+
# Takes in a string with the contents of the last-run file, outputs a string with its new contents
|
122
|
+
def update_last_run_file_string(file_str)
|
123
|
+
|
124
|
+
# Parses the dates in .last-run
|
125
|
+
date_strings = []
|
126
|
+
file_str.each_line do |date_str|
|
127
|
+
date_strings << date_str
|
128
|
+
end
|
129
|
+
dates = date_strings.map do |date_str|
|
130
|
+
Date.strptime(date_str, @date_format)
|
131
|
+
end
|
132
|
+
|
133
|
+
#Generates new dates
|
134
|
+
new_dates = []
|
135
|
+
|
136
|
+
if dates.length == 0
|
137
|
+
new_dates = [@current_date]
|
138
|
+
|
139
|
+
elsif dates.length == 1
|
140
|
+
if dates[0] == @current_date
|
141
|
+
new_dates = [dates[0]]
|
142
|
+
else
|
143
|
+
new_dates = [@current_date, dates[0]]
|
144
|
+
end
|
145
|
+
|
146
|
+
else
|
147
|
+
if dates[0] == @current_date
|
148
|
+
new_dates = [dates[0], dates[1]]
|
149
|
+
else
|
150
|
+
new_dates = [@current_date, dates[0]]
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# Generates a new file string
|
155
|
+
new_file_str = ""
|
156
|
+
new_dates.each do
|
157
|
+
|date|
|
158
|
+
new_file_str += "#{date.strftime(@date_format)}\n"
|
159
|
+
end
|
160
|
+
|
161
|
+
# Returns [new_file_str, new_dates]
|
162
|
+
return new_file_str, new_dates
|
163
|
+
end
|
164
|
+
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'pathname'
|
3
|
+
|
4
|
+
class Torque
|
5
|
+
|
6
|
+
##
|
7
|
+
# Creates a (limited) interface through which Torque can interact with the local file system
|
8
|
+
#
|
9
|
+
# Supports:
|
10
|
+
#
|
11
|
+
# * File creation, reading, line-by-line iteration, and overwriting
|
12
|
+
#
|
13
|
+
# * Directory creation
|
14
|
+
#
|
15
|
+
# * Pathname checking
|
16
|
+
#
|
17
|
+
class FileSystem
|
18
|
+
|
19
|
+
def initialize
|
20
|
+
# Do nothing. The file system's properties are automatically global
|
21
|
+
end
|
22
|
+
|
23
|
+
##
|
24
|
+
# Creates a file, optionally overwriting an existing file to do so
|
25
|
+
def file_create(filename, over=false)
|
26
|
+
if !path_exist? filename || over
|
27
|
+
File.open(filename, "w")
|
28
|
+
else
|
29
|
+
raise IOError.new "File #{filename} already exists"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
##
|
34
|
+
# Returns an iterator over each line of a file
|
35
|
+
def file_each_line(filename)
|
36
|
+
File.open(filename, "r").each_line
|
37
|
+
end
|
38
|
+
|
39
|
+
##
|
40
|
+
# Returns the contents of a file
|
41
|
+
def file_read(filename)
|
42
|
+
File.read(filename)
|
43
|
+
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# Writes a string to file
|
47
|
+
def file_write(filename, string)
|
48
|
+
File.write(filename, string)
|
49
|
+
end
|
50
|
+
|
51
|
+
##
|
52
|
+
# Creates a directory
|
53
|
+
def mkdir(dirname)
|
54
|
+
FileUtils.mkdir(dirname)
|
55
|
+
end
|
56
|
+
|
57
|
+
##
|
58
|
+
# Creates a directory, creating intermediate directories as needed
|
59
|
+
def mkdir_p(dirname)
|
60
|
+
FileUtils.mkdir_p(dirname)
|
61
|
+
end
|
62
|
+
|
63
|
+
##
|
64
|
+
# Returns true if a pathname exists, else false
|
65
|
+
def path_exist?(pathname)
|
66
|
+
Pathname.new(pathname).exist?
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'mail'
|
2
|
+
|
3
|
+
class Torque
|
4
|
+
|
5
|
+
##
|
6
|
+
# Handles sending the finished release notes document to a list of emails after Torque generates it
|
7
|
+
class Mailer
|
8
|
+
|
9
|
+
##
|
10
|
+
# @param email An email address
|
11
|
+
# @param password A password that matches the email address given
|
12
|
+
#
|
13
|
+
# Creates a Mailer that will send emails from the email address/password combo given
|
14
|
+
def initialize(email, password)
|
15
|
+
@email = email
|
16
|
+
@password = password
|
17
|
+
|
18
|
+
options = { :address => "smtp.gmail.com",
|
19
|
+
:port => 587,
|
20
|
+
:domain => 'gmail.com',
|
21
|
+
:user_name => @email,
|
22
|
+
:password => @password,
|
23
|
+
:authentication => 'plain',
|
24
|
+
:enable_starttls_auto => true }
|
25
|
+
|
26
|
+
Mail.defaults do
|
27
|
+
delivery_method :smtp, options
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# ##
|
32
|
+
# # Returns true if the email and password combination is valid, else false
|
33
|
+
# def verify
|
34
|
+
# end
|
35
|
+
|
36
|
+
# @param notes_string The release notes file in string form
|
37
|
+
# @param subject_line The subject line to use in the email
|
38
|
+
# @param address_list A comma-separated list of email addresses to which to send the notes
|
39
|
+
#
|
40
|
+
# Sends notes_string as an email with subject_line from an arbitrary email address to everyone on address_list
|
41
|
+
def send_notes(notes_string, subject_line, address_list)
|
42
|
+
|
43
|
+
mail = Mail::Message.new
|
44
|
+
|
45
|
+
mail.charset = "UTF-8"
|
46
|
+
mail.to address_list
|
47
|
+
mail.from @email
|
48
|
+
mail.subject subject_line
|
49
|
+
mail.body notes_string
|
50
|
+
|
51
|
+
begin
|
52
|
+
mail.deliver
|
53
|
+
rescue Net::SMTPAuthenticationError
|
54
|
+
# TODO Remove this output. Should fail silently and be replaced by an independent call to :verify
|
55
|
+
puts "Username and password not accepted by Gmail. Check your username and password or try using a Gmail " \
|
56
|
+
"account"
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require_relative 'error/invalid_project_error'
|
3
|
+
require_relative 'error/invalid_token_error'
|
4
|
+
require_relative 'error/pivotal_api_error'
|
5
|
+
|
6
|
+
class Torque
|
7
|
+
class Pivotal
|
8
|
+
|
9
|
+
##
|
10
|
+
# @param token A Pivotal Tracker api token
|
11
|
+
def initialize(token)
|
12
|
+
@token = token
|
13
|
+
end
|
14
|
+
|
15
|
+
##
|
16
|
+
# Returns true if a connection to www.pivotaltracker.com exists, else false
|
17
|
+
def self.connection?
|
18
|
+
begin
|
19
|
+
TCPSocket.new "www.pivotaltracker.com", 80
|
20
|
+
true
|
21
|
+
rescue SocketError
|
22
|
+
false
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
##
|
27
|
+
# Returns true if the token supplied is a valid token, else false
|
28
|
+
def check_token
|
29
|
+
begin
|
30
|
+
get_project_data
|
31
|
+
true
|
32
|
+
rescue InvalidTokenError
|
33
|
+
false
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
##
|
38
|
+
# Sends a request through the Pivotal Tracker API
|
39
|
+
#
|
40
|
+
# Returns a string of html data from Pivotal Tracker with data on the stories for the given project
|
41
|
+
def get_project_stories(project)
|
42
|
+
|
43
|
+
# Polls story data from pivotal tracker
|
44
|
+
host="pivotaltracker.com"
|
45
|
+
port=80
|
46
|
+
url="http://www.pivotaltracker.com/services/v3/projects/#{project}/stories"
|
47
|
+
headers={'X-TrackerToken'=>@token}
|
48
|
+
|
49
|
+
response=Net::HTTP.new(host, port).get(url, headers)
|
50
|
+
|
51
|
+
# Handles network errors
|
52
|
+
if response.code == "401"
|
53
|
+
raise InvalidTokenError.new "The Pivotal Tracker API token supplied is not valid for project #{project}"
|
54
|
+
|
55
|
+
elsif response.code == "500"
|
56
|
+
raise InvalidProjectError.new "The Pivotal Tracker project ID supplied, #{project}, is invalid"
|
57
|
+
|
58
|
+
elsif response.code != "200"
|
59
|
+
raise PivotalAPIError.new(
|
60
|
+
"The Pivotal Tracker API responded with an unexpected error code: #{response.code}. Check your " \
|
61
|
+
+ "project ID, API token, and/or internet connection"
|
62
|
+
)
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
response.body
|
67
|
+
end
|
68
|
+
|
69
|
+
##
|
70
|
+
# Sends a request through the Pivotal Tracker API
|
71
|
+
#
|
72
|
+
# Returns a string of html data from Pivotal Tracker with data on each of a user's projects
|
73
|
+
def get_project_data
|
74
|
+
|
75
|
+
host="pivotaltracker.com"
|
76
|
+
port=80
|
77
|
+
url="http://www.pivotaltracker.com/services/v3/projects"
|
78
|
+
headers={'X-TrackerToken'=>@token}
|
79
|
+
|
80
|
+
response=Net::HTTP.new(host, port).get(url, headers)
|
81
|
+
|
82
|
+
# Handles network errors
|
83
|
+
if response.code == "401"
|
84
|
+
raise InvalidTokenError.new "The Pivotal Tracker API token supplied is not valid"
|
85
|
+
|
86
|
+
elsif response.code != "200"
|
87
|
+
raise PivotalAPIError.new(
|
88
|
+
"The Pivotal Tracker API responded with an unexpected error code: #{response.code}. Check your " \
|
89
|
+
"API token, and/or internet connection"
|
90
|
+
)
|
91
|
+
end
|
92
|
+
|
93
|
+
response.body
|
94
|
+
end
|
95
|
+
|
96
|
+
|
97
|
+
end
|
98
|
+
end
|