codefumes 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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