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,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
|