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
data/bin/project
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# Manages user projects for Torque. Displays the list of Pivotal Tracker projects that can be accessed by a given
|
4
|
+
# token, or automatically changes the project when given a project name or ID
|
5
|
+
|
6
|
+
lib_dir = File.expand_path("../lib", File.dirname(__FILE__))
|
7
|
+
working_dir = '.'
|
8
|
+
|
9
|
+
require 'optparse'
|
10
|
+
require 'pathname'
|
11
|
+
require_relative "#{lib_dir}/torque/pivotal"
|
12
|
+
require_relative "#{lib_dir}/torque/torque_info_parser"
|
13
|
+
require_relative "#{lib_dir}/torque/error/missing_torque_info_file_error"
|
14
|
+
require_relative "#{lib_dir}/torque/project/project"
|
15
|
+
require_relative "#{lib_dir}/torque/project/project_manager"
|
16
|
+
|
17
|
+
|
18
|
+
# Handles arguments
|
19
|
+
|
20
|
+
options = {}
|
21
|
+
option_parser = OptionParser.new do |opts|
|
22
|
+
|
23
|
+
help_message = "Changes the current Pivotal Tracker project to the one specified, or lists the projects available " \
|
24
|
+
"with the current API token when run with no arguments"
|
25
|
+
|
26
|
+
opts.banner = "\nUsage:"
|
27
|
+
opts.banner += "\n torque project [ID]"
|
28
|
+
opts.banner += "\n torque project [-n/--name NAME]"
|
29
|
+
opts.banner += "\n"
|
30
|
+
opts.banner += "\n#{help_message}"
|
31
|
+
opts.banner += "\n"
|
32
|
+
opts.banner += "\n"
|
33
|
+
|
34
|
+
opts.on("-h", "--help", "Displays this help screen") do
|
35
|
+
|arg|
|
36
|
+
options[:help] = arg
|
37
|
+
end
|
38
|
+
|
39
|
+
opts.on("-n", "--name NAME", "Switches the Torque project to the project named NAME") do
|
40
|
+
|arg|
|
41
|
+
options[:name] = arg
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
# Handles invalid parsing errors
|
47
|
+
|
48
|
+
begin
|
49
|
+
option_parser.parse!
|
50
|
+
|
51
|
+
rescue OptionParser::InvalidOption, OptionParser::MissingArgument
|
52
|
+
puts $!.to_s
|
53
|
+
puts option_parser
|
54
|
+
exit
|
55
|
+
end
|
56
|
+
|
57
|
+
# Handles special cases and exceptions
|
58
|
+
|
59
|
+
if (ARGV.member? "help") || options[:help]
|
60
|
+
puts "(Ran with 'help' option; other options ignored)"
|
61
|
+
puts option_parser
|
62
|
+
exit
|
63
|
+
end
|
64
|
+
|
65
|
+
# Checks for a connection to Pivotal Tracker
|
66
|
+
|
67
|
+
if !Torque::Pivotal.connection?
|
68
|
+
puts "ABORTING"
|
69
|
+
puts "Cannot connect to www.pivotaltracker.com. A connection is required to use Torque"
|
70
|
+
exit 1
|
71
|
+
end
|
72
|
+
|
73
|
+
# Locates project identifiers
|
74
|
+
|
75
|
+
project_identifiers = ARGV
|
76
|
+
|
77
|
+
is_name = false
|
78
|
+
|
79
|
+
if options[:name]
|
80
|
+
is_name = true
|
81
|
+
project_identifiers << options[:name]
|
82
|
+
end
|
83
|
+
|
84
|
+
if project_identifiers.size == 0
|
85
|
+
|
86
|
+
project_manager = Torque::ProjectManager.new
|
87
|
+
begin
|
88
|
+
project_manager.load_project_list
|
89
|
+
rescue Torque::MissingTorqueInfoFileError => e
|
90
|
+
puts "ABORTING"
|
91
|
+
puts e.message
|
92
|
+
puts "Run 'torque config' in this directory, or change your working directory"
|
93
|
+
exit 2
|
94
|
+
end
|
95
|
+
|
96
|
+
puts project_manager.format_project_list
|
97
|
+
|
98
|
+
elsif project_identifiers.size == 1
|
99
|
+
|
100
|
+
# Throw error if cannot find a matching project
|
101
|
+
if !Pathname.new("./.torqueinfo.yaml").exist?
|
102
|
+
puts "Directory is not configured for torque (.torqueinfo.yaml file is missing). Run 'torque config' in this " \
|
103
|
+
+ "directory, or change your working directory"
|
104
|
+
exit
|
105
|
+
end
|
106
|
+
|
107
|
+
# Switch to a new project
|
108
|
+
identifier = project_identifiers[0]
|
109
|
+
|
110
|
+
project_manager = Torque::ProjectManager.new
|
111
|
+
project_manager.load_project_list
|
112
|
+
|
113
|
+
if is_name
|
114
|
+
project = project_manager.project_list.select { |project| project.name == identifier }[0]
|
115
|
+
else
|
116
|
+
project = project_manager.project_list.select { |project| project.id == identifier }[0]
|
117
|
+
end
|
118
|
+
|
119
|
+
if project.nil?
|
120
|
+
if is_name
|
121
|
+
puts "No project named #{identifier}"
|
122
|
+
else
|
123
|
+
puts "No project with id #{identifier}"
|
124
|
+
end
|
125
|
+
puts
|
126
|
+
puts project_manager.format_project_list
|
127
|
+
exit
|
128
|
+
end
|
129
|
+
|
130
|
+
Torque::TorqueInfoParser.new.set("project", project.id)
|
131
|
+
puts "Switched to project #{project.id} - '#{project.name}'"
|
132
|
+
|
133
|
+
else
|
134
|
+
# Too many project identifiers. Throw error
|
135
|
+
puts "Only 1 project allowed, found #{project_identifiers.size}: #{project_identifiers.to_s}"
|
136
|
+
end
|
data/bin/torque
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
lib_dir = File.expand_path("../lib", File.dirname(__FILE__))
|
4
|
+
exec_dir = File.expand_path(File.dirname(__FILE__))
|
5
|
+
|
6
|
+
require 'net/http'
|
7
|
+
require 'optparse'
|
8
|
+
require "#{lib_dir}/torque"
|
9
|
+
require "#{lib_dir}/torque/pivotal"
|
10
|
+
require "#{lib_dir}/torque/error/invalid_project_error"
|
11
|
+
require "#{lib_dir}/torque/error/invalid_token_error"
|
12
|
+
require "#{lib_dir}/torque/error/missing_project_error"
|
13
|
+
require "#{lib_dir}/torque/error/missing_token_error"
|
14
|
+
require "#{lib_dir}/torque/error/pivotal_api_error"
|
15
|
+
|
16
|
+
|
17
|
+
# Parses the options
|
18
|
+
|
19
|
+
options = {}
|
20
|
+
|
21
|
+
option_parser = OptionParser.new do |opts|
|
22
|
+
|
23
|
+
help_message = "This script will fail unless it is run in a directory that was configured for Torque by running " \
|
24
|
+
"'torque config' (see 'torque config --help' for more). To change the directory torque runs in, change your " \
|
25
|
+
"current working directory, or use the --root option"
|
26
|
+
|
27
|
+
opts.banner = "\nUsage:"
|
28
|
+
opts.banner += "\n torque [options]"
|
29
|
+
opts.banner += "\n"
|
30
|
+
opts.banner += "\n#{help_message}"
|
31
|
+
opts.banner += "\n"
|
32
|
+
opts.banner += "\nSpecial commands:"
|
33
|
+
opts.banner += "\n config Configures a directory for use with Torque"
|
34
|
+
opts.banner += "\n email Sets up user email, adds to/removes from Torque's mailing list"
|
35
|
+
opts.banner += "\n project Displays & switches between available Pivotal Tracker projects"
|
36
|
+
opts.banner += "\n"
|
37
|
+
opts.banner += "\nOptions:"
|
38
|
+
opts.banner += "\n"
|
39
|
+
|
40
|
+
opts.on("--email", "Email the compiled notes to the email list in .torqueinfo") do
|
41
|
+
|arg|
|
42
|
+
options[:email] = arg
|
43
|
+
end
|
44
|
+
|
45
|
+
opts.on("-f,", "--from FROM_DATE", "Set the date (inclusive) from which to accept Pivotal stories") do
|
46
|
+
|arg|
|
47
|
+
options[:accept_from] = arg
|
48
|
+
end
|
49
|
+
|
50
|
+
opts.on("-h", "--help", "Displays this help screen") do
|
51
|
+
|arg|
|
52
|
+
options[:help] = arg
|
53
|
+
end
|
54
|
+
|
55
|
+
opts.on("--root ROOT_DIR", "Set the root directory for the script (should contain a .torqueinfo.yaml file)") do
|
56
|
+
|arg|
|
57
|
+
options[:root_dir] = arg
|
58
|
+
end
|
59
|
+
|
60
|
+
opts.on("-s", "--silent", "Run silently (no output) (overrides -v)") do
|
61
|
+
|arg|
|
62
|
+
options[:silent] = arg
|
63
|
+
end
|
64
|
+
|
65
|
+
opts.on("-t,", "--to TO_DATE", "Set the date (inclusive) until which to accept Pivotal stories") do
|
66
|
+
|arg|
|
67
|
+
options[:accept_to] = arg
|
68
|
+
end
|
69
|
+
|
70
|
+
opts.on("-v", "--verbose", "Run verbosely") do
|
71
|
+
|arg|
|
72
|
+
options[:verbose] = arg
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
# Handles special commands
|
78
|
+
|
79
|
+
arg_string = (ARGV.length <= 1 ? "" : ARGV[1..ARGV.length].join(" "))
|
80
|
+
|
81
|
+
if(ARGV[0]=="config")
|
82
|
+
exec("#{exec_dir}/config #{arg_string}")
|
83
|
+
|
84
|
+
elsif(ARGV[0]=="email")
|
85
|
+
exec("#{exec_dir}/email #{arg_string}")
|
86
|
+
|
87
|
+
elsif(ARGV[0]=="project")
|
88
|
+
exec("#{exec_dir}/project #{arg_string}")
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
# Handles special cases and exceptions
|
93
|
+
|
94
|
+
begin
|
95
|
+
option_parser.parse!
|
96
|
+
|
97
|
+
if (ARGV.member? "help") || options[:help]
|
98
|
+
puts "(Ran with 'help' option; other options ignored)"
|
99
|
+
puts option_parser.help
|
100
|
+
exit
|
101
|
+
|
102
|
+
elsif !ARGV.empty?
|
103
|
+
puts "Unknown arguments: #{ARGV.join(", ")}"
|
104
|
+
puts option_parser.help
|
105
|
+
exit
|
106
|
+
end
|
107
|
+
|
108
|
+
rescue OptionParser::InvalidOption, OptionParser::MissingArgument
|
109
|
+
puts $!.to_s
|
110
|
+
puts option_parser.help
|
111
|
+
exit
|
112
|
+
end
|
113
|
+
|
114
|
+
# Checks for a connection to Pivotal Tracker
|
115
|
+
|
116
|
+
if !Torque::Pivotal.connection?
|
117
|
+
puts "ABORTING"
|
118
|
+
puts "Cannot connect to www.pivotaltracker.com. A connection is required to use Torque"
|
119
|
+
exit 1
|
120
|
+
end
|
121
|
+
|
122
|
+
# Runs Torque, handling errors
|
123
|
+
|
124
|
+
begin
|
125
|
+
Torque.new(options).execute
|
126
|
+
rescue Torque::MissingTokenError, Torque::InvalidTokenError => e
|
127
|
+
puts "ABORTING"
|
128
|
+
puts e.message
|
129
|
+
puts "Run 'torque config --token' to set your Pivotal Tracker API token"
|
130
|
+
(e.is_a? Torque::MissingTokenError) ? (exit 2) : (exit 3)
|
131
|
+
rescue Torque::MissingProjectError, Torque::InvalidProjectError => e
|
132
|
+
puts "ABORTING"
|
133
|
+
puts e.message
|
134
|
+
puts "Run 'torque project' to set your Pivotal Tracker project"
|
135
|
+
(e.is_a? Torque::MissingProjectError) ? (exit 4) : (exit 5)
|
136
|
+
rescue Torque::PivotalAPIError => e
|
137
|
+
puts "ABORTING"
|
138
|
+
puts e.message
|
139
|
+
exit 6
|
140
|
+
rescue Torque::MissingTorqueInfoFileError => e
|
141
|
+
puts "ABORTING"
|
142
|
+
puts e.message
|
143
|
+
puts "Run 'torque config' in this directory, or change your working directory"
|
144
|
+
exit 7
|
145
|
+
rescue Torque::MissingOutputDirectoryError => e
|
146
|
+
puts "ABORTING"
|
147
|
+
puts e.message
|
148
|
+
puts "Run 'torque config -o [directory]' to create/change your output directory"
|
149
|
+
exit 8
|
150
|
+
end
|
data/lib/torque.rb
ADDED
@@ -0,0 +1,190 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'pathname'
|
3
|
+
require_relative 'torque/pivotal'
|
4
|
+
require_relative 'torque/pivotal_html_parser'
|
5
|
+
require_relative 'torque/mailer'
|
6
|
+
require_relative 'torque/settings'
|
7
|
+
require_relative 'torque/torque_info_parser'
|
8
|
+
require_relative 'torque/project/project'
|
9
|
+
require_relative 'torque/project/project_manager'
|
10
|
+
|
11
|
+
##
|
12
|
+
# The central class for Torque. Takes in a hash of options, generates release notes from online data, writes them to
|
13
|
+
# file and emails them
|
14
|
+
class Torque
|
15
|
+
|
16
|
+
##
|
17
|
+
# @param options A hash of the settings to use for
|
18
|
+
# @param settings An instance of Torque::Settings (default: Torque::Settings.new)
|
19
|
+
# @param settings An instance of Torque::FileSystem (default: Torque::FileSystem.new)
|
20
|
+
#
|
21
|
+
# Creates a new instance of Torque
|
22
|
+
def initialize(options, settings=nil, fs=nil)
|
23
|
+
@fs = fs || FileSystem.new
|
24
|
+
@settings = settings || Settings.new(options, @fs)
|
25
|
+
|
26
|
+
@date_format = "%Y/%m/%d"
|
27
|
+
end
|
28
|
+
|
29
|
+
##
|
30
|
+
# @param project_html An html string containing all the project's stories
|
31
|
+
# @param project_name The name of the current project
|
32
|
+
#
|
33
|
+
# Returns a string comprising the release notes document
|
34
|
+
def generate_notes(project_html, project_name)
|
35
|
+
|
36
|
+
notes_string = ""
|
37
|
+
|
38
|
+
# Parses stories from html, filters them by date and returns them in an array
|
39
|
+
stories = PivotalHTMLParser.new.process_project_date_filter(project_html, @settings.accept_from, @settings.accept_to)
|
40
|
+
|
41
|
+
# Adds the header
|
42
|
+
notes_string += Date.today.strftime(@date_format)+"\n"
|
43
|
+
notes_string += "Release Notes\n"
|
44
|
+
notes_string += "Project #{@settings.project} - '#{project_name}'\n"
|
45
|
+
if @settings.custom_date_range
|
46
|
+
notes_string += "(custom "+@settings.accept_from.strftime(@date_format)+" - "+@settings.accept_to.strftime(@date_format)+")\n"
|
47
|
+
else
|
48
|
+
notes_string += "("+@settings.accept_from.strftime(@date_format)+" - "+@settings.accept_to.strftime(@date_format)+")\n"
|
49
|
+
end
|
50
|
+
notes_string += "\nThese notes were generated with Torque, a Pivotal Tracker command line utility"
|
51
|
+
notes_string += "\n"
|
52
|
+
notes_string += "\n"
|
53
|
+
|
54
|
+
# Adds each story
|
55
|
+
stories.sort! { |s1, s2| -(s1.date_accepted <=> s2.date_accepted) }
|
56
|
+
stories.length.times do |i|
|
57
|
+
story = stories[i]
|
58
|
+
|
59
|
+
notes_string += "#{story.story_id}\n"
|
60
|
+
notes_string += "#{story.name}\n"
|
61
|
+
notes_string += "Accepted on "+story.date_accepted.strftime(@date_format)+"\n"
|
62
|
+
notes_string += "https://www.pivotaltracker.com/story/show/#{story.story_id}\n"
|
63
|
+
|
64
|
+
print_if_verbose "[Torque] (#{story.date_accepted.strftime(@date_format)}) #{story.name}"
|
65
|
+
|
66
|
+
descArray = story.description.split("\n")
|
67
|
+
descArray.length.times do |i|
|
68
|
+
notes_string += "\t"+descArray[i]+"\n"
|
69
|
+
end
|
70
|
+
|
71
|
+
notes_string += "\n"
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
print_if_verbose "[Torque]"
|
76
|
+
print_if_verbose "[Torque] Added #{stories.length} stories"
|
77
|
+
|
78
|
+
notes_string
|
79
|
+
end
|
80
|
+
|
81
|
+
##
|
82
|
+
# @param notes_string A string representation of the generated release notes
|
83
|
+
# @param project_name The name of the current project
|
84
|
+
# @param mailer An instance of Torque::Mailer (default: Torque::Mailer.new(@settings.email_address, @settings.email_password) )
|
85
|
+
#
|
86
|
+
# Emails notes_string to the email list specified in the torque info file
|
87
|
+
def email_notes(notes_string, project_name, mailer=nil)
|
88
|
+
print_if_not_silent "Emailing notes..."
|
89
|
+
|
90
|
+
mailer ||= Mailer.new(@settings.email_address, @settings.email_password)
|
91
|
+
|
92
|
+
if @settings.email_address.blank?
|
93
|
+
print_if_not_silent "Emailing notes failed: No sender email address found. (Set this with 'torque email')"
|
94
|
+
elsif @settings.email_password.blank?
|
95
|
+
print_if_not_silent "Emailing notes failed: No sender email password found. (Set this with 'torque email')"
|
96
|
+
elsif(@settings.email_to.length == 0)
|
97
|
+
print_if_not_silent "No email addresses found. (Add them with 'torque email')"
|
98
|
+
else
|
99
|
+
|
100
|
+
address_list = ""
|
101
|
+
@settings.email_to.each_with_index {
|
102
|
+
|address, i|
|
103
|
+
|
104
|
+
if i!=0
|
105
|
+
address_list << ", "
|
106
|
+
end
|
107
|
+
address_list << address
|
108
|
+
}
|
109
|
+
|
110
|
+
subject_line = \
|
111
|
+
"Release Notes: Project #{@settings.project} - '#{project_name}' (" \
|
112
|
+
+ (@settings.custom_date_range ? "Custom " : "") \
|
113
|
+
+ "#{@settings.accept_from.strftime('%Y/%m/%d')} - " \
|
114
|
+
+ "#{@settings.accept_to.strftime('%Y/%m/%d')}" \
|
115
|
+
+ ")"
|
116
|
+
|
117
|
+
mailer.send_notes(notes_string, subject_line, address_list)
|
118
|
+
|
119
|
+
print_if_not_silent "Emailed notes to #{address_list}"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
##
|
124
|
+
# The method run by Torque on the command line. Generates the notes, writes them to file, optionally emails them
|
125
|
+
def execute
|
126
|
+
|
127
|
+
# Determines the current project
|
128
|
+
|
129
|
+
torque_info = TorqueInfoParser.new(@settings.torque_info_path)
|
130
|
+
project_manager = ProjectManager.new(torque_info)
|
131
|
+
|
132
|
+
project_manager.load_project_list
|
133
|
+
project_name = project_manager.current_project.name
|
134
|
+
|
135
|
+
# Generates the notes
|
136
|
+
|
137
|
+
print_if_not_silent "Current project: #{@settings.project} - '#{project_name}'"
|
138
|
+
|
139
|
+
print_if_not_silent "Generating release notes: " \
|
140
|
+
+ (@settings.custom_date_range ? "Custom " : "") \
|
141
|
+
+ @settings.accept_from.strftime(@date_format) + " - " \
|
142
|
+
+ @settings.accept_to.strftime(@date_format) \
|
143
|
+
+ "..."
|
144
|
+
|
145
|
+
project_html = Pivotal.new(@settings.token).get_project_stories(@settings.project)
|
146
|
+
notes_string = generate_notes(project_html, project_name)
|
147
|
+
|
148
|
+
# Writes the notes to file
|
149
|
+
|
150
|
+
root_dir_path = Pathname.new(@settings.root_dir)
|
151
|
+
rel_notes_path = Pathname.new(@settings.current_notes_path).relative_path_from(root_dir_path)
|
152
|
+
print_if_not_silent "- Writing notes > #{rel_notes_path}"
|
153
|
+
|
154
|
+
@fs.file_write("#{@settings.current_notes_path}", notes_string)
|
155
|
+
|
156
|
+
# Creates a record of the notes file
|
157
|
+
|
158
|
+
rel_record_path = Pathname.new(@settings.record_path).relative_path_from(root_dir_path)
|
159
|
+
print_if_not_silent "- Copying notes > #{rel_record_path}"
|
160
|
+
|
161
|
+
begin
|
162
|
+
@fs.file_write("#{@settings.record_path}", notes_string)
|
163
|
+
rescue => e
|
164
|
+
err_str = "WARNING: Unable to make a record of the release notes file. Could not locate records "
|
165
|
+
err_str += "directory \"#{File.expand_path(@settings.record_path)}\""
|
166
|
+
print_if_not_silent err_str
|
167
|
+
end
|
168
|
+
|
169
|
+
print_if_not_silent "Notes generated!"
|
170
|
+
|
171
|
+
# Emails the release notes to all specified addresses
|
172
|
+
|
173
|
+
email_notes(notes_string, project_name) if(@settings.email)
|
174
|
+
|
175
|
+
end
|
176
|
+
|
177
|
+
|
178
|
+
private
|
179
|
+
|
180
|
+
# Prints a message if silent is not on
|
181
|
+
def print_if_not_silent(msg)
|
182
|
+
puts msg unless @settings.silent
|
183
|
+
end
|
184
|
+
|
185
|
+
# Prints a message if verbose is on and silent is not
|
186
|
+
def print_if_verbose(msg)
|
187
|
+
print_if_not_silent msg if @settings.verbose
|
188
|
+
end
|
189
|
+
|
190
|
+
end
|