Tracker 0.1.2 → 1.0.0

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/README CHANGED
@@ -1,18 +1,45 @@
1
- == Using tracker (or whatever it's named)
2
- tracker help
3
- Can be used with alternative file, use the -f option.
4
- tracker -f /path/to/sqlite3_file
5
-
6
- == Dependencies
7
- Ruby 1.8
8
- SWIG
9
- SQLite3
10
- SQLite3-Ruby interface
11
-
12
- == To add a new action
13
- * Implement it in Bug class (for whatever bug needs that doesn't have)
14
- * Add the command to the factory in lib/factory.rb
15
- * Make sure the options are valid. You can add new options in the hash in tracker.rb.
16
-
17
- == LICENSE
18
- See MIT-LICENSE.
1
+ == What is Tracker? ==
2
+
3
+ Tracker 1.0 is a remake of Tracker (see http://tracker.rubyforge.org). It's going to be a leaner/quicker ticket tracker.
4
+
5
+ Tracker is meant for personal projects where you want to keep track of bugs that pop up or features. The database is a local sqlite3 file that can be uploaded into your Subversion repository.
6
+
7
+ This program and all software is released under the MIT License. Feel free to use it, modify it, steal from it, etc. for personal or commercial use.
8
+
9
+
10
+ == About ==
11
+
12
+ Author: Hugh Bien
13
+ Email: bienhd@gmail.com
14
+ Guide: http://recursive2.wordpress.com/2007/05/11/re-introducing-tracker/
15
+ Repository: http://recursive2.com/svn/public/tracker
16
+ Changelog: Use `svn log`
17
+
18
+ Below are checklists for the upcoming milestone and BDD checklist. The milestone checklist shows what features I'm aiming for. The BDD checklist is sort of a break down of the feature I'm currently working on.
19
+
20
+ To see a complete list of features and bugs, use `trac list` on the .tracker_db for this project.
21
+
22
+
23
+ == Version 1.1.0 Milestone :: May 20, 2007 ==
24
+
25
+ [ ] trac help 'command' should spit out help messages for each command
26
+ [ ] Extract the class 'Output' from 'Tracker'.
27
+ [ ] HTML output report
28
+ [ ] specifications for the 'trac' binary
29
+ [ ] bug fixes from v1.0.0
30
+
31
+
32
+ == BDD Checklist ==
33
+
34
+ [ ] trac help
35
+ [ ] trac help bootstrap
36
+ [ ] trac help list
37
+ [ ] trac help search
38
+ [ ] trac help create
39
+ [ ] trac help show
40
+ [ ] trac help update
41
+ [ ] trac help delete
42
+ [ ] trac help add
43
+ [ ] trac help remove
44
+ [ ] trac help open
45
+ [ ] trac help close
@@ -0,0 +1,52 @@
1
+ require 'rake'
2
+ require 'spec/rake/spectask'
3
+ require 'rake/gempackagetask'
4
+ require 'rake/clean'
5
+
6
+ CLEAN.include('pkg', 'doc')
7
+
8
+ gem_spec = Gem::Specification.new do |s|
9
+ s.name = "Tracker"
10
+ s.version = "1.0.0"
11
+ s.summary = "A command line ticket tracker for solo projects."
12
+
13
+ # Spec Files
14
+ s.test_files = FileList['specs/*_spec.rb']
15
+
16
+ # Executables
17
+ s.executables = ['trac']
18
+
19
+ # Other related files
20
+ s.files = FileList['README', 'Rakefile', 'lib/*.rb']
21
+ end
22
+
23
+ namespace :build do
24
+ Rake::GemPackageTask.new(gem_spec) do |pkg|
25
+ pkg.need_zip = false
26
+ pkg.need_tar = false
27
+ end
28
+ end
29
+
30
+ namespace :spec do
31
+ desc "Runs all specifications"
32
+ Spec::Rake::SpecTask.new('all') do |t|
33
+ t.spec_files = FileList['spec/*_spec.rb']
34
+ end
35
+
36
+ desc "Generate specdocs for examples"
37
+ Spec::Rake::SpecTask.new('doc') do |t|
38
+ t.spec_files = FileList['spec/*_spec.rb']
39
+ t.spec_opts = ["--format", "s"]
40
+ end
41
+
42
+ desc "Generate HTML report for specifications"
43
+ Spec::Rake::SpecTask.new('html') do |t|
44
+ `spec -f html spec/*_spec.rb > site/spec.html`
45
+ end
46
+
47
+ desc "Runs autotest using RSpec"
48
+ task :autotest do
49
+ require './spec/rspec_autotest'
50
+ RspecAutotest.run
51
+ end
52
+ end
data/bin/trac CHANGED
@@ -1,65 +1,82 @@
1
1
  #!/usr/bin/env ruby
2
- # Tracker is under the MIT-LICENSE
3
- # Please see http://ieng6.ucsd.edu/~hbien/tracker for more information about Tracker.
2
+ require File.dirname(__FILE__) + "/../lib/environment.rb"
4
3
 
5
- # Handles connection to the database
4
+ params = {}
6
5
 
7
- require File.dirname(__FILE__) + "/../lib/interface.rb"
8
- require File.dirname(__FILE__) + "/../lib/database.rb"
9
-
10
- # The default database file_name
11
- DEFAULT_DATABASE_FILE_NAME = ".tracker_db"
12
-
13
- # -----------------------------------------------------------------------------
14
- # Begin executing the script here.
15
- # -----------------------------------------------------------------------------
16
-
17
- # What file should we use for the database?
18
- database = DEFAULT_DATABASE_FILE_NAME
19
-
20
- # What to do if no database can be found
21
- def no_database_error(database_file = "the current")
22
- puts "Could not find '.tracker_db' in #{database_file} or any parent directories."
23
- puts "Try '#{File.basename(__FILE__)} create' to create a new database in the current directory or '#{File.basename(__FILE__)} help' to see available commands."
24
- exit
25
- end
6
+ parser = OptionParser.new do |opts|
7
+ opts.banner = "Usage: #{$0} <subcommand> [options]\n" +
8
+ "Tracker is a ticket tracker for personal projects.\n" +
9
+ "\nAvailable Subcommands:\n" +
10
+ " bootstrap\n" +
11
+ " list (--open | --closed)\n" +
12
+ " search PHRASE\n" +
13
+ " create --message STRING --keywords STRING\n" +
14
+ " show ID\n" +
15
+ " update ID --message STRING --keywords STRING\n" +
16
+ " delete ID\n" +
17
+ " add ID --message STRING\n" +
18
+ " remove ID --position INT\n" +
19
+ " open ID\n" +
20
+ " close ID\n\n"
21
+
22
+ opts.on("-m", "--message [STRING]", "sets the message of your ticket or comment") do |message|
23
+ params[:message] = message
24
+ end
26
25
 
27
- # Is the user creating a new database?
28
- if ARGV.include?("create") && !ARGV.include?("help")
29
- if File.exists?(database)
30
- puts "A '.tracker_db' database already exists in the current directory. Try removing it to create a new one."
26
+ opts.on("-k", "--keywords [STRING]", "sets the keywords of your ticket") do |keywords|
27
+ params[:keywords] = keywords
28
+ end
29
+
30
+ opts.on("-i", "--id [INT]", "sets the id of the ticket you want") do |id|
31
+ params[:id] = id
32
+ end
33
+
34
+ opts.on("-p", "--position [INT]", "position of a comment") do |position|
35
+ params[:position] = position
36
+ end
37
+
38
+ opts.on("-l", "--limit [INT]", "limit the number of tickets to list or search for") do |limit|
39
+ params[:limit] = limit
40
+ end
41
+
42
+ opts.on("-o", "--open", "list open tickets only") do
43
+ params[:state] = Ticket::OPEN
44
+ end
45
+
46
+ opts.on("-c", "--closed", "list closed tickets only") do
47
+ params[:state] = Ticket::CLOSED
48
+ end
49
+
50
+ opts.on("-h", "--help", "display this help menu and exit") do
51
+ puts opts
31
52
  exit
32
- else
33
- Database::create_database(database)
53
+ end
54
+
55
+ opts.on("-v", "--version", "shows version of Tracker (v1.0.0)") do
56
+ puts "Tracker v1.0.0"
34
57
  exit
35
58
  end
36
59
  end
37
60
 
38
- # Find the database in parent directories
39
- iteration = 0
40
- while !File.exists?(database) && iteration < 10
41
- iteration += 1
42
- database = File.join("..", database)
61
+ # For unknown commands
62
+ unless %w[bootstrap list search create show update delete add remove open close -h --help -v --version].include?(ARGV[0])
63
+ puts "Unknown command: '#{ARGV[0]}'\n" +
64
+ "Type '#{$0} --help' for usage."
65
+ exit
43
66
  end
44
67
 
45
- # Could we find the database?
46
- if iteration == 10 && !ARGV.include?("help") && !ARGV.include?("-h") && !ARGV.include?("--help") && !ARGV.include?("--file") && !ARGV.include?("-f")
47
- no_database_error
48
- else
49
- if ARGV.include?("-f") || ARGV.include?("--file")
50
- index = ARGV.index("-f") + 1 if ARGV.include?("-f")
51
- index = ARGV.index("--file") + 1 if ARGV.include?("--file")
52
- database = ARGV[index]
53
- database = File.join(database, ".tracker_db") if File.directory?(database)
54
- no_database_error(database) if !File.exist?(database)
55
- end
56
- # if we did, execute command
57
- Database::connect_to_database(database)
58
- parser = Interface.new
59
- begin
60
- parser.parse_command(ARGV)
61
- rescue Exception => error
62
- puts "Error executing database instructions: " + error
68
+ begin
69
+ # Runs the application
70
+ parser.parse!(ARGV)
71
+ tracker = Tracker.new
72
+
73
+ # Turn ID into ARGV[1] if necessary
74
+ if %w[show update delete search add remove open close].include?(ARGV[0])
75
+ params[:id] ||= ARGV[1]
63
76
  end
77
+
78
+ # Print the results
79
+ puts tracker.send(ARGV[0], params)
80
+ rescue ActiveRecord::StatementInvalid
81
+ puts "Database error. Use 'trac bootstrap' to create a new database."
64
82
  end
65
-
@@ -0,0 +1,7 @@
1
+ class Comment < ActiveRecord::Base
2
+ belongs_to :ticket
3
+ acts_as_list :scope => :ticket
4
+ after_save do |comment|
5
+ comment.ticket ? comment.ticket.save! : nil
6
+ end
7
+ end
@@ -0,0 +1,31 @@
1
+ require "rubygems"
2
+ require "active_record"
3
+ require "stringio"
4
+ require "optparse"
5
+
6
+ require File.dirname(__FILE__) + "/ticket.rb"
7
+ require File.dirname(__FILE__) + "/comment.rb"
8
+ require File.dirname(__FILE__) + "/tracker.rb"
9
+
10
+ # Find .tracker_db
11
+ module Environment
12
+ # Search for the database unless you're doing a bootstrap
13
+ if ARGV[0] != "bootstrap" && ENV['TRACKER_ENV'] != "testing"
14
+ directory = "."
15
+ i = 0
16
+ 6.times do
17
+ break if File.exists?(directory + "/.tracker_db")
18
+ directory += "/.."
19
+ i += 1
20
+ end
21
+ directory = nil if i == 6
22
+ end
23
+
24
+ DIRECTORY = directory || "."
25
+ DATABASE = ENV['TRACKER_ENV'] == "testing" ? DIRECTORY + "/.tracker_test_db" : DIRECTORY + "/.tracker_db"
26
+ end
27
+
28
+ ActiveRecord::Base.establish_connection(
29
+ :adapter => "sqlite3",
30
+ :database => Environment::DATABASE
31
+ )
@@ -0,0 +1,38 @@
1
+ class Ticket < ActiveRecord::Base
2
+ has_many :comments, :order => "position", :dependent => :delete_all
3
+
4
+ OPEN, CLOSED = 0, 1
5
+
6
+ def self.search(phrase, options = {})
7
+ conditions = { :state => (options.delete(:state) || OPEN) }
8
+ self.with_scope(:find => options.merge(:conditions => conditions) ) do
9
+ self.find(:all, :conditions => ["keywords LIKE ? OR message LIKE ?", "%" + phrase + "%", "%" + phrase + "%"])
10
+ end
11
+ end
12
+
13
+ def summary
14
+ digits = self.number.to_s.length
15
+ trim_to = 65 - digits
16
+ self.message.length > 60 ? self.message[0..trim_to] + "..." : self.message
17
+ end
18
+
19
+ def open
20
+ self.update_attributes!(:state => OPEN)
21
+ end
22
+
23
+ def close
24
+ self.update_attributes!(:state => CLOSED)
25
+ end
26
+
27
+ def open?
28
+ self.state == 0
29
+ end
30
+
31
+ def closed?
32
+ self.state == 1
33
+ end
34
+
35
+ def number
36
+ self.id
37
+ end
38
+ end
@@ -0,0 +1,127 @@
1
+ class Tracker
2
+ def bootstrap(params = {})
3
+ $stdout = StringIO.new
4
+ ActiveRecord::Schema.define do
5
+ create_table "tickets" do |t|
6
+ t.column "keywords", :string
7
+ t.column "message", :text
8
+ t.column "state", :int, :default => 0
9
+ t.column "updated_at", :datetime
10
+ end
11
+
12
+ create_table "comments" do |t|
13
+ t.column "ticket_id", :int
14
+ t.column "position", :int
15
+ t.column "message", :text
16
+ end
17
+ end
18
+ result = "New database '#{Environment::DATABASE}' was created"
19
+ rescue ActiveRecord::StatementInvalid => e
20
+ result = "Could not create the database '#{Environment::DATABASE}' because it already exists"
21
+ ensure
22
+ $stdout = STDOUT
23
+ return result
24
+ end
25
+
26
+ def create(params = {})
27
+ ticket = Ticket.create(params)
28
+ "Created new ticket ##{ticket.number}"
29
+ end
30
+
31
+ def show(params = {})
32
+ find_ticket(params[:id]) do |ticket|
33
+ show_ticket(ticket)
34
+ end
35
+ end
36
+
37
+ def list(params = {})
38
+ conditions = { :state => Ticket::OPEN }.merge(params)
39
+ tickets = Ticket.with_scope(:find => { :conditions => conditions }) do
40
+ Ticket.find(:all, :order => "updated_at DESC")
41
+ end
42
+
43
+ return list_tickets(tickets)
44
+ end
45
+
46
+ def search(params = {})
47
+ phrase = params.delete(:id)
48
+ tickets = Ticket.search(phrase, params.merge(:order => "updated_at DESC"))
49
+
50
+ return list_tickets(tickets)
51
+ end
52
+
53
+ def update(params = {})
54
+ find_ticket(params[:id]) do |ticket|
55
+ params.delete(:id)
56
+ ticket.update_attributes!(params)
57
+ "Ticket ##{ticket.number} was updated"
58
+ end
59
+ end
60
+
61
+ def delete(params = {})
62
+ find_ticket(params[:id]) do |ticket|
63
+ ticket.destroy
64
+ "Ticket ##{ticket.number} was deleted"
65
+ end
66
+ end
67
+
68
+ def add(params = {})
69
+ find_ticket(params[:id]) do |ticket|
70
+ ticket.comments.create(:message => params[:message])
71
+ "New comment was added to ticket ##{ticket.number}"
72
+ end
73
+ end
74
+
75
+ def remove(params = {})
76
+ find_ticket(params[:id]) do |ticket|
77
+ ticket.comments[params[:position] - 1].destroy
78
+ "Comment ##{params[:position]} was removed from ticket ##{ticket.number}"
79
+ end
80
+ end
81
+
82
+ def open(params = {})
83
+ find_ticket(params[:id]) do |ticket|
84
+ ticket.open
85
+ "Opened ticket ##{ticket.number}"
86
+ end
87
+ end
88
+
89
+ def close(params = {})
90
+ find_ticket(params[:id]) do |ticket|
91
+ ticket.close
92
+ "Closed ticket ##{ticket.number}"
93
+ end
94
+ end
95
+
96
+ private
97
+ def find_ticket(ticket_id)
98
+ ticket = Ticket.find_by_id(ticket_id)
99
+ if ticket
100
+ yield ticket
101
+ else
102
+ "Could not find ticket ##{ticket_id}"
103
+ end
104
+ end
105
+
106
+ def list_tickets(tickets)
107
+ if tickets == nil || tickets.size == 0
108
+ "There are currently no tickets in the database"
109
+ elsif tickets.size == 1
110
+ show_ticket(tickets[0])
111
+ else
112
+ tickets.reverse.map do |ticket|
113
+ "##{ticket.number}:#{" " * (5 - ticket.number.to_s.length)}#{ticket.summary}"
114
+ end.join("\n")
115
+ end
116
+ end
117
+
118
+ def show_ticket(ticket)
119
+ "Ticket: ##{ticket.number}\n" +
120
+ (ticket.keywords.blank? ? "" : "Keywords: #{ticket.keywords}\n") +
121
+ "Messages: - #{ticket.updated_at.to_s(:short)}\n" +
122
+ " * - #{ticket.message}\n" +
123
+ ticket.comments.map do |comment|
124
+ " #{comment.position} - #{comment.message}"
125
+ end.join("\n")
126
+ end
127
+ end