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