codefumes 0.1.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/History.txt ADDED
@@ -0,0 +1,4 @@
1
+ == 0.0.1 2009-04-24
2
+
3
+ * 1 major enhancement:
4
+ * Initial release
data/Manifest.txt ADDED
@@ -0,0 +1,24 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.txt
4
+ Rakefile
5
+ config/website.yml.sample
6
+ lib/codefumes.rb
7
+ lib/codefumes/api.rb
8
+ lib/codefumes/commit.rb
9
+ lib/codefumes/config_file.rb
10
+ lib/codefumes/payload.rb
11
+ lib/codefumes/project.rb
12
+ spec/codefumes/api_spec.rb
13
+ spec/codefumes/commit_spec.rb
14
+ spec/codefumes/config_file_spec.rb
15
+ spec/codefumes/payload_spec.rb
16
+ spec/codefumes/project_spec.rb
17
+ spec/spec.opts
18
+ spec/spec_helper.rb
19
+ tasks/rspec.rake
20
+ website/index.html
21
+ website/index.txt
22
+ website/javascripts/rounded_corners_lite.inc.js
23
+ website/stylesheets/screen.css
24
+ website/template.html.erb
data/README.txt ADDED
@@ -0,0 +1,102 @@
1
+ = codefumes
2
+
3
+ http://www.codefumes.com
4
+
5
+ == DESCRIPTION:
6
+
7
+ CodeFumes.com[http://codefumes.com] is a service intended to help people
8
+ involved with software projects who are interested in tracking, sharing,
9
+ and reviewing metrics/information about a project in relation to the
10
+ commits of said project's repository. The site supports a small set of
11
+ 'standard' metrics, but also provides a simple method of supplying
12
+ and retrieving custom metrics, allowing users to gather any metric they
13
+ are interested in tracking.
14
+
15
+ The 'codefumes' gem is an implementation of the
16
+ CodeFumes.com[http://codefumes.com] API. The intention of the
17
+ gem is to simplify integration with CodeFumes.com for developers of
18
+ other libraries & and applications.
19
+
20
+ For an example of another library using the current features of this
21
+ gem, you can refer to the
22
+ 'codefumes_harvester[http://codefumes.rubyforge.org/codefumes_harvester]' gem.
23
+
24
+ == FEATURES/PROBLEMS:
25
+
26
+ === Features
27
+ * Saving, finding, marshalling, and destroying CodeFumes
28
+ projects
29
+ * Associating and retrieving a repository's history of commits for a
30
+ CodeFumes 'project'
31
+ * Simple interface for accessing both CodeFumes's 'standard' commit
32
+ metrics, as well as custom commit attributes; simplifying
33
+ integration with other tools & libraries users may be interested in
34
+ using.
35
+ * Interfaces with the CodeFumes config file (used to track projects a
36
+ user has created on the site)
37
+
38
+ === Problems / Things to Note
39
+
40
+ * CodeFumes 'projects' are repository-specific, not branch-specific.
41
+
42
+ == SYNOPSIS:
43
+
44
+ require 'codefumes'
45
+
46
+ # Creating & finding a CodeFumes project
47
+ p = Project.save # optionally providing a custom public key: :public_key => 'Abc3'
48
+ found_p = Project.find(p.public_key)
49
+ p.pulic_key # => 'Abc3'
50
+ p.api_uri # => 'http://codefumes.com/api/v1/xml/Abc3'
51
+
52
+ # Commits
53
+ c = Commit.find(<commit identifier>)
54
+ c.identifier # => git commit SHA (svn support coming soon)
55
+ c.short_message # => commit message
56
+
57
+ # Custom attributes associated with a commit
58
+ c.custom_attributes[:coverage] # => "80"
59
+
60
+
61
+ # Payloads, used to break up large HTTP requests
62
+ content = Payload.prepare(payload_content)
63
+ content.each {|chunk| chunk.save}
64
+
65
+ == REQUIREMENTS:
66
+
67
+ * httparty (0.4.3)
68
+
69
+ == INSTALL:
70
+
71
+ From RubyForge:
72
+
73
+ sudo gem install codefumes
74
+
75
+ Or, from Github:
76
+
77
+ sudo gem install cosyn-codefumes
78
+
79
+ == LICENSE:
80
+
81
+ (The MIT License)
82
+
83
+ Copyright (c) 2009 Cosyn Technologies, Inc.
84
+
85
+ Permission is hereby granted, free of charge, to any person obtaining
86
+ a copy of this software and associated documentation files (the
87
+ 'Software'), to deal in the Software without restriction, including
88
+ without limitation the rights to use, copy, modify, merge, publish,
89
+ distribute, sublicense, and/or sell copies of the Software, and to
90
+ permit persons to whom the Software is furnished to do so, subject to
91
+ the following conditions:
92
+
93
+ The above copyright notice and this permission notice shall be
94
+ included in all copies or substantial portions of the Software.
95
+
96
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
97
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
98
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
99
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
100
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
101
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
102
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,31 @@
1
+ %w[hoe rake rake/clean fileutils rubigen hoe].each { |f| require f }
2
+
3
+ require File.dirname(__FILE__) + '/lib/codefumes'
4
+
5
+ begin
6
+ require "hanna/rdoctask"
7
+ rescue LoadError
8
+ require 'rake/rdoctask'
9
+ end
10
+
11
+ # Load in the harvester ane metric_fu gems if available so we can collect metrics
12
+ begin
13
+ require "metric_fu"
14
+ require "codefumes_harvester"
15
+ rescue LoadError
16
+ end
17
+
18
+ $hoe = Hoe.spec('codefumes') do |p|
19
+ p.developer('Cosyn Technologies', 'devs@codefumes.com')
20
+ p.summary = "API gem for the CodeFumes website"
21
+ p.extra_deps = [
22
+ ['httparty','>= 0.4.3']
23
+ ]
24
+ p.extra_dev_deps = [
25
+ ['jscruggs-metric_fu', ">= 1.1.5"],
26
+ ]
27
+ end
28
+
29
+ Dir['tasks/**/*.rake'].each { |t| load t}
30
+
31
+ task :default => [:spec]
@@ -0,0 +1,2 @@
1
+ host: unknown@rubyforge.org
2
+ remote_dir: /var/www/gforge-projects/codefumes
data/lib/codefumes.rb ADDED
@@ -0,0 +1,11 @@
1
+ require 'httparty'
2
+
3
+ require 'codefumes/api'
4
+ require 'codefumes/project'
5
+ require 'codefumes/config_file'
6
+ require 'codefumes/payload'
7
+ require 'codefumes/commit'
8
+
9
+ module CodeFumes
10
+ VERSION = '0.1.0'
11
+ end
@@ -0,0 +1,19 @@
1
+ module CodeFumes
2
+ class API
3
+ include HTTParty
4
+
5
+ format :xml
6
+
7
+ BASE_URIS = {
8
+ :production => 'http://www.codefumes.com/api/v1/xml',
9
+ :test => 'http://test.codefumes.com/api/v1/xml'
10
+ }
11
+
12
+ def self.mode(mode)
13
+ base_uri(BASE_URIS[mode]) if BASE_URIS[mode]
14
+ end
15
+
16
+ mode(:production)
17
+
18
+ end
19
+ end
@@ -0,0 +1,144 @@
1
+ module CodeFumes
2
+ # Similar to a revision control system, a Commit encompasses a set of
3
+ # changes to a codebase, who made them, when said changes were applied
4
+ # to the previous revision of codebase, et cetera.
5
+ #
6
+ # A Commit has a concept of 'standard attributes' which will always be
7
+ # present in a response from CodeFumes.com[http://codefumes.com], such
8
+ # as the +identifier+, +author+, and +commit_message+ (see the list of
9
+ # attributes for a comprehensive listing). In addition to this, users
10
+ # are able to associate 'custom attributes' to a Commit, allowing
11
+ # users to link any number of attributes with a commit identifier and
12
+ # easily retrieve them later.
13
+ #
14
+ # One thing to note about Commit objects is that they are read-only.
15
+ # To associate metrics with a Commit object, a Payload object should
16
+ # be created and saved. Refer to the Payload documentation for more
17
+ # information.
18
+ class Commit < CodeFumes::API
19
+ attr_reader :identifier, :author_name, :author_email, :committer_name,
20
+ :committer_email, :short_message, :message,:committed_at,
21
+ :authored_at, :uploaded_at, :api_uri, :parent_identifiers,
22
+ :line_additions, :line_deletions, :line_total,
23
+ :affected_file_count, :custom_attributes
24
+
25
+ # Instantiates a new Commit object
26
+ #
27
+ # Accepts a Hash of options, including:
28
+ # * identifier
29
+ # * author_email
30
+ # * author_name
31
+ # * committer_email
32
+ # * committer_name
33
+ # * short_message
34
+ # * message
35
+ # * committed_at
36
+ # * authored_at
37
+ # * uploaded_at
38
+ # * api_uri
39
+ # * parent_identifiers
40
+ # * line_additions
41
+ # * line_deletions
42
+ # * line_total
43
+ # * affected_file_count
44
+ # * custom_attributes
45
+ #
46
+ # +custom_attributes+ should be a Hash of attribute_name/value
47
+ # pairs associated with the commit. All other attributes are
48
+ # expected to be String values, other than +committed_at+ and
49
+ # +authored_at+, which are expected to be DateTime objects.
50
+ # Technically speaking, you could pass anything you wanted into
51
+ # the fields, but when using with the CodeFumes API, the attribute
52
+ # values will be of the type String, DateTime, or Hash.
53
+ def initialize(options)
54
+ @identifier = options["identifier"]
55
+ @author_email = options["author_email"]
56
+ @author_name = options["author_name"]
57
+ @committer_email = options["committer_email"]
58
+ @committer_name = options["committer_name"]
59
+ @short_message = options["short_message"]
60
+ @message = options["message"]
61
+ @committed_at = options["committed_at"]
62
+ @authored_at = options["authored_at"]
63
+ @uploaded_at = options["uploaded_at"]
64
+ @api_uri = options["api_uri"]
65
+ @parent_identifiers = options["parent_identifiers"]
66
+ @line_additions = options["line_additions"]
67
+ @line_deletions = options["line_deletions"]
68
+ @line_total = options["line_total"]
69
+ @affected_file_count = options["affected_file_count"]
70
+ @custom_attributes = options["custom_attributes"] || {}
71
+ convert_custom_attributes_keys_to_symbols
72
+ end
73
+
74
+ # Returns the name of the author and the email associated
75
+ # with the commit in a string formatted as:
76
+ # "Name [email_address]"
77
+ # (ie: "John Doe [jdoe@example.com]")
78
+ def author
79
+ "#{author_name} [#{author_email}]"
80
+ end
81
+
82
+ # Returns the name of the committer and the email associated
83
+ # with the commit in a string formatted as:
84
+ # "Name [email_address]"
85
+ # (ie: "John Doe [jdoe@example.com]")
86
+ def committer
87
+ "#{committer_name} [#{committer_email}]"
88
+ end
89
+
90
+ # Returns the Commit object associated with the supplied identifier.
91
+ # Returns nil if the identifier is not found.
92
+ def self.find(identifier)
93
+ response = get("/commits/#{identifier}")
94
+ case response.code
95
+ when 200
96
+ return nil if response["commit"].empty?
97
+ new(response["commit"])
98
+ else
99
+ nil
100
+ end
101
+ end
102
+
103
+ # Returns a collection of commits associated with the specified
104
+ # Project public key.
105
+ def self.all(project_public_key)
106
+ response = get("/projects/#{project_public_key}/commits")
107
+ case response.code
108
+ when 200
109
+ return [] if response["commits"].empty? || response["commits"]["commit"].nil?
110
+ response["commits"]["commit"].map do |commit_data|
111
+ new(commit_data)
112
+ end
113
+ else
114
+ nil
115
+ end
116
+ end
117
+
118
+ # Returns the most recent commit associated with the specified
119
+ # Project public key.
120
+ def self.latest(project_public_key)
121
+ response = get("/projects/#{project_public_key}/commits/latest")
122
+ case response.code
123
+ when 200
124
+ new(response["commit"])
125
+ else
126
+ nil
127
+ end
128
+ end
129
+
130
+ # Returns the commit identifier of the most recent commit of with
131
+ # the specified Project public key.
132
+ def self.latest_identifier(project_public_key)
133
+ latest_commit = latest(project_public_key)
134
+ latest_commit.nil? ? nil : latest_commit.identifier
135
+ end
136
+
137
+ private
138
+ def convert_custom_attributes_keys_to_symbols
139
+ @custom_attributes = @custom_attributes.inject({}) do |results, key_and_value|
140
+ results.merge! key_and_value.first.to_sym => key_and_value.last
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,85 @@
1
+ module CodeFumes
2
+ # CodeFumes uses a global (per-user) config file to store relevant
3
+ # information around a user's use of the service. Doing so addresses
4
+ # the following goals:
5
+ # * A defined location for all tools to utilize which contains URI's
6
+ # and and keys for all projects a user has set up on
7
+ # CodeFumes.com[http://codefumes.com], simplifying integration.
8
+ # * Associating (or disassociating) a project to (or from) a CodeFumes
9
+ # project does not require any modifications to said project's
10
+ # repository.
11
+ # * Simplified the implementation of 'user-less' projects on the
12
+ # website.
13
+ #
14
+ # This class wraps up reading and writing this config file so other
15
+ # developers should not have to concern themselves with how to
16
+ # serialize & write the data of a project into the appropriate format,
17
+ # output file, et cetera.
18
+ class ConfigFile
19
+ DEFAULT_FILE_STRUCTURE = {}
20
+ DEFAULT_PATH = File.expand_path('~/.codefumes_config')
21
+
22
+ class << self
23
+ # Returns the path to the CodeFumes global (per-user) config file.
24
+ # The default path is '~/.codefumes_config'.
25
+ def path
26
+ @path || ENV['CODEFUMES_CONFIG_FILE'] || DEFAULT_PATH.dup
27
+ end
28
+
29
+ # Sets the path which should be used for storing the configuration
30
+ # CodeFumes.com data.
31
+ def path=(custom_path)
32
+ @path = custom_path.nil? ? path : File.expand_path(custom_path)
33
+ end
34
+
35
+ # Store the supplied project into the CodeFumes config file.
36
+ def save_project(project)
37
+ config = serialized
38
+ if config[:projects]
39
+ config[:projects].merge!(project.to_config)
40
+ else
41
+ config[:projects] = project.to_config
42
+ end
43
+ write(config)
44
+ end
45
+
46
+ # Remove the supplied project from the CodeFumes config file.
47
+ def delete_project(project)
48
+ config = serialized
49
+ config[:projects] && config[:projects].delete(project.public_key.to_sym)
50
+ write(config)
51
+ end
52
+
53
+ # Returns a Hash representation of the CodeFumes config file
54
+ def serialized
55
+ empty? ? DEFAULT_FILE_STRUCTURE.dup : loaded
56
+ end
57
+
58
+ # Returns a Hash representation of a specific project contained in
59
+ # the CodeFumes config file.
60
+ def options_for_project(public_key)
61
+ config = serialized
62
+ public_key && config[:projects] && config[:projects][public_key.to_sym] || {}
63
+ end
64
+
65
+ private
66
+ def write(serializable_object)
67
+ File.open(path, 'w') do |f|
68
+ f.puts YAML::dump(serializable_object)
69
+ end
70
+ end
71
+
72
+ def exists?
73
+ File.exists?(path)
74
+ end
75
+
76
+ def empty?
77
+ !(exists? && loaded)
78
+ end
79
+
80
+ def loaded
81
+ YAML::load_file(path)
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,103 @@
1
+ module CodeFumes
2
+ # Payloads are intended to simplify sending up large amounts of
3
+ # content at one time. For example, when sending up the entire
4
+ # history of a repository, making a POST request for each commit would
5
+ # require a very large number of requests. Using a Payload object
6
+ # allows larger amounts of content to be saved at one time,
7
+ # significantly reducing the number of requests made.
8
+ class Payload < CodeFumes::API
9
+ PAYLOAD_CHARACTER_LIMIT = 4000 #:nodoc:
10
+
11
+ attr_reader :project_public_key, :project_private_key, :created_at
12
+
13
+ # Accepts +:public_key+, +:private_key+, and :content keys.
14
+ # +:content+ should also contain a key named +:commits+, with a list
15
+ # of commits and associated data. An example would be:
16
+ #
17
+ # {:public_key => "abC3", :private_key => "some-private-key",
18
+ # :content => {:commits => [{:identifier => "commit_identifer",
19
+ # :files_affected => 3, :any_metric_you_want => "value"}]}}
20
+ def initialize(options = {})
21
+ @project_public_key = options[:public_key]
22
+ @project_private_key = options[:private_key]
23
+ @content = options[:content]
24
+ end
25
+
26
+ # Saves instance to CodeFumes.com. After a successful save, the
27
+ # +created_at+ attribute will be populated with the timestamp the
28
+ # Payload was created.
29
+ #
30
+ # Returns +true+ if the Payload does not contain any content to be
31
+ # saved or the request was successful.
32
+ #
33
+ # Returns +false+ if the request failed.
34
+ def save
35
+ return true if empty_payload?
36
+ response = self.class.post("/projects/#{@project_public_key}/payloads", :query => {:payload => @content}, :basic_auth => {:username => @project_public_key, :password => @project_private_key})
37
+
38
+ case response.code
39
+ when 201
40
+ @created_at = response['payload']['created_at']
41
+ true
42
+ else
43
+ false
44
+ end
45
+ end
46
+
47
+ # +save+ requests are made with a standard POST request (not a
48
+ # multi-part POST), so the request size is limited by the
49
+ # application server. The current configuration on CodeFumes.com
50
+ # limits requests to approximately 8,000 bytes (a little over). In
51
+ # order to simplify dealing with these constraints, without
52
+ # requiring a multi-part POST request, +prepare+ can be used to
53
+ # "chunk" up the data into Payloads which do not exceed this limit.
54
+ #
55
+ # Returns collection of payload objects which fall into the
56
+ # constraints of a individual payload (ie: length of raw request,
57
+ # et cetera).
58
+ #--
59
+ # TODO: Clean up how the size of the request is constrained, this
60
+ # is pretty hackish right now (basically guesses how many
61
+ # characters would be added when HTTParty wraps the content in XML.
62
+ def self.prepare(data = {})
63
+ return [] if data.nil? || data.empty?
64
+ raw_payload = data.dup
65
+
66
+ public_key = raw_payload.delete(:public_key)
67
+ raise ArgumentError, "No public key provided" if public_key.nil?
68
+
69
+ private_key = raw_payload.delete(:private_key)
70
+
71
+ if raw_payload[:content].nil? || raw_payload[:content][:commits].nil?
72
+ raise ArgumentError, "No commits key provided"
73
+ end
74
+
75
+ content = raw_payload[:content][:commits]
76
+ initial_chunks = {:on_deck => [], :prepared => []}
77
+
78
+ # TODO: Clean this up
79
+ chunked = content.inject(initial_chunks) do |chunks, new_commit|
80
+ if chunks[:on_deck].to_s.length + new_commit.to_s.length >= PAYLOAD_CHARACTER_LIMIT
81
+ chunks[:prepared] << chunks[:on_deck]
82
+ chunks[:on_deck] = [new_commit]
83
+ elsif new_commit == content.last
84
+ chunks[:on_deck] << new_commit
85
+ chunks[:prepared] << chunks[:on_deck]
86
+ chunks[:on_deck] = []
87
+ else
88
+ chunks[:on_deck] << new_commit
89
+ end
90
+ chunks
91
+ end
92
+
93
+ chunked[:prepared].map do |raw_content|
94
+ Payload.new(:public_key => public_key, :private_key => private_key, :content => {:commits => raw_content})
95
+ end
96
+ end
97
+
98
+ private
99
+ def empty_payload?
100
+ @content.empty? || @content[:commits].nil? || @content[:commits].blank?
101
+ end
102
+ end
103
+ end