codefumes 0.1.10 → 0.2.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.
Files changed (68) hide show
  1. data/Gemfile +19 -0
  2. data/Gemfile.lock +135 -0
  3. data/History.txt +12 -0
  4. data/LICENSE +20 -0
  5. data/Manifest.txt +40 -19
  6. data/README.txt +11 -29
  7. data/Rakefile +15 -10
  8. data/bin/fumes +214 -0
  9. data/config/website.yml +2 -0
  10. data/cucumber.yml +2 -0
  11. data/features/claiming_a_project.feature +46 -0
  12. data/features/deleting_a_project.feature +32 -0
  13. data/features/releasing_a_project.feature +50 -0
  14. data/features/step_definitions/cli_steps.rb +98 -0
  15. data/features/step_definitions/common_steps.rb +168 -0
  16. data/features/step_definitions/filesystem_steps.rb +19 -0
  17. data/features/storing_user_api_key.feature +41 -0
  18. data/features/support/common.rb +29 -0
  19. data/features/support/env.rb +24 -0
  20. data/features/support/matchers.rb +11 -0
  21. data/features/synchronizing_repository_with_project.feature +33 -0
  22. data/lib/codefumes.rb +10 -8
  23. data/lib/codefumes/api.rb +20 -11
  24. data/lib/codefumes/api/build.rb +139 -0
  25. data/lib/codefumes/api/claim.rb +74 -0
  26. data/lib/codefumes/api/commit.rb +150 -0
  27. data/lib/codefumes/api/payload.rb +93 -0
  28. data/lib/codefumes/api/project.rb +158 -0
  29. data/lib/codefumes/cli_helpers.rb +54 -0
  30. data/lib/codefumes/config_file.rb +3 -2
  31. data/lib/codefumes/errors.rb +21 -0
  32. data/lib/codefumes/exit_codes.rb +10 -0
  33. data/lib/codefumes/harvester.rb +113 -0
  34. data/lib/codefumes/quick_build.rb +43 -0
  35. data/lib/codefumes/quick_metric.rb +20 -0
  36. data/lib/codefumes/source_control.rb +137 -0
  37. data/lib/integrity_notifier/codefumes.haml +11 -0
  38. data/lib/integrity_notifier/codefumes.rb +62 -0
  39. data/spec/codefumes/{build_spec.rb → api/build_spec.rb} +14 -24
  40. data/spec/codefumes/{claim_spec.rb → api/claim_spec.rb} +42 -3
  41. data/spec/codefumes/{commit_spec.rb → api/commit_spec.rb} +34 -24
  42. data/spec/codefumes/api/payload_spec.rb +148 -0
  43. data/spec/codefumes/api/project_spec.rb +286 -0
  44. data/spec/codefumes/api_spec.rb +38 -15
  45. data/spec/codefumes/config_file_spec.rb +69 -13
  46. data/spec/codefumes/harvester_spec.rb +118 -0
  47. data/spec/codefumes/source_control_spec.rb +199 -0
  48. data/spec/codefumes_service_helpers.rb +23 -19
  49. data/spec/fixtures/sample_project_dirs/no_scm/description +4 -0
  50. data/spec/spec_helper.rb +1 -0
  51. data/tasks/cucumber.rake +11 -0
  52. metadata +145 -60
  53. data/bin/cf_claim_project +0 -9
  54. data/bin/cf_release_project +0 -10
  55. data/bin/cf_store_credentials +0 -10
  56. data/lib/cf_claim_project/cli.rb +0 -95
  57. data/lib/cf_release_project/cli.rb +0 -76
  58. data/lib/cf_store_credentials/cli.rb +0 -50
  59. data/lib/codefumes/build.rb +0 -131
  60. data/lib/codefumes/claim.rb +0 -57
  61. data/lib/codefumes/commit.rb +0 -144
  62. data/lib/codefumes/payload.rb +0 -103
  63. data/lib/codefumes/project.rb +0 -129
  64. data/spec/cf_claim_project/cli_spec.rb +0 -17
  65. data/spec/cf_release_project/cli_spec.rb +0 -41
  66. data/spec/cf_store_credentials/cli_spec.rb +0 -28
  67. data/spec/codefumes/payload_spec.rb +0 -155
  68. data/spec/codefumes/project_spec.rb +0 -274
@@ -0,0 +1,93 @@
1
+ module CodeFumes
2
+ module API
3
+ # Payloads are intended to simplify sending up large amounts of
4
+ # content at one time. For example, when sending up the entire
5
+ # history of a repository, making a POST request for each commit would
6
+ # require a very large number of requests. Using a Payload object
7
+ # allows larger amounts of content to be saved at one time,
8
+ # significantly reducing the number of requests made.
9
+ class Payload
10
+ PAYLOAD_CHARACTER_LIMIT = 4000 #:nodoc:
11
+
12
+ attr_reader :project, :created_at
13
+
14
+ # +:commit_data+ should be a list of commits and associated data.
15
+ # An example would be:
16
+ #
17
+ # [{:identifier => "commit_identifer", :files_affected => 3,
18
+ # :custom_attributes => {:any_metric_you_want => "value"}}]
19
+ def initialize(project, commit_data)
20
+ @project = project
21
+ @content = {:commits => commit_data}
22
+ end
23
+
24
+ # Saves instance to CodeFumes.com. After a successful save, the
25
+ # +created_at+ attribute will be populated with the timestamp the
26
+ # Payload was created.
27
+ #
28
+ # Returns +true+ if the Payload does not contain any content to be
29
+ # saved or the request was successful.
30
+ #
31
+ # Returns +false+ if the request failed.
32
+ def save
33
+ return true if empty_payload?
34
+ response = API.post("/projects/#{@project.public_key}/payloads", :query => {:payload => @content}, :basic_auth => {:username => @project.public_key, :password => @project.private_key})
35
+
36
+ case response.code
37
+ when 201
38
+ @created_at = response['payload']['created_at']
39
+ true
40
+ else
41
+ false
42
+ end
43
+ end
44
+
45
+ # +save+ requests are made with a standard POST request (not a
46
+ # multi-part POST), so the request size is limited by the
47
+ # application server. The current configuration on CodeFumes.com
48
+ # limits requests to approximately 8,000 bytes (a little over). In
49
+ # order to simplify dealing with these constraints, without
50
+ # requiring a multi-part POST request, +prepare+ can be used to
51
+ # "chunk" up the data into Payloads which do not exceed this limit.
52
+ #
53
+ # Returns collection of payload objects which fall into the
54
+ # constraints of a individual payload (ie: length of raw request,
55
+ # et cetera).
56
+ #--
57
+ # TODO: Clean up how the size of the request is constrained, this
58
+ # is pretty hackish right now (basically guesses how many
59
+ # characters would be added when HTTParty wraps the content in XML.)
60
+ def self.prepare(project, commit_data = {})
61
+ return [] if commit_data.nil? || commit_data.empty?
62
+ raw_payload = commit_data.dup
63
+
64
+ content = raw_payload[:commits]
65
+ initial_chunks = {:on_deck => [], :prepared => []}
66
+
67
+ # TODO: Clean this up
68
+ chunked = content.inject(initial_chunks) do |chunks, new_commit|
69
+ if chunks[:on_deck].to_s.length + new_commit.to_s.length >= PAYLOAD_CHARACTER_LIMIT
70
+ chunks[:prepared] << chunks[:on_deck]
71
+ chunks[:on_deck] = [new_commit]
72
+ elsif new_commit == content.last
73
+ chunks[:on_deck] << new_commit
74
+ chunks[:prepared] << chunks[:on_deck]
75
+ chunks[:on_deck] = []
76
+ else
77
+ chunks[:on_deck] << new_commit
78
+ end
79
+ chunks
80
+ end
81
+
82
+ chunked[:prepared].map do |raw_content|
83
+ Payload.new(project, raw_content)
84
+ end
85
+ end
86
+
87
+ private
88
+ def empty_payload?
89
+ @content.nil? || @content.empty? || @content[:commits].nil? || @content[:commits].blank?
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,158 @@
1
+ module CodeFumes
2
+ module API
3
+ # A Project encapsulates the concept of a project on the CodeFumes.com
4
+ # website. Each project has a public key, private key, and can have a
5
+ # name defined. Projects are also associated with a collection of
6
+ # commits from a repository.
7
+ class Project
8
+ attr_reader :private_key, :short_uri, :community_uri, :api_uri, :build_status
9
+ attr_accessor :name, :public_key
10
+
11
+ def initialize(public_key=nil, private_key = nil, options = {})
12
+ @public_key = public_key
13
+ @private_key = private_key
14
+ @name = options['name'] || options[:name]
15
+ @short_uri = options['short_uri'] || options[:short_uri]
16
+ @community_uri = options['community_uri'] || options[:community_uri]
17
+ @api_uri = options['api_uri'] || options[:api_uri]
18
+ @build_status = options['build_status'] || options[:build_status]
19
+ end
20
+
21
+ # Creates new project
22
+ def self.create
23
+ response = API.post('/projects')
24
+
25
+ case response.code
26
+ when 201
27
+ new.reinitialize_from_hash!(response['project'])
28
+ else
29
+ false
30
+ end
31
+ end
32
+
33
+ # Deletes project from the website. You must have both the +public_key+
34
+ # and +private_key+ of a project in order to delete it.
35
+ #
36
+ # Returns +true+ if the request succeeded.
37
+ #
38
+ # Returns +false+ if the request failed.
39
+ def delete
40
+ if public_key.nil? || private_key.nil?
41
+ msg = "You must have both the private key & public key of a project in order to delete it. (currently: {:private_key => '#{private_key.to_s}', :public_key => '#{public_key.to_s}'}"
42
+ raise Errors::InsufficientCredentials, msg
43
+ end
44
+
45
+ response = destroy!
46
+ case response.code
47
+ when 200
48
+ return true
49
+ else
50
+ return false
51
+ end
52
+ end
53
+
54
+ # Attempts to save current state of project to CodeFumes.
55
+ #
56
+ # Returns +true+ if the request succeeded.
57
+ #
58
+ # Returns +false+ if the request failed.
59
+ def save
60
+ response = API.put("/projects/#{public_key}", :query => {:project => {:name => name}},
61
+ :basic_auth => {:username => public_key, :password => private_key})
62
+
63
+ case response.code
64
+ when 200
65
+ reinitialize_from_hash!(response['project'])
66
+ true
67
+ else
68
+ false
69
+ end
70
+ end
71
+
72
+ # Serializes a Project instance to a format compatible with the
73
+ # CodeFumes config file.
74
+ def to_config
75
+ project_attributes = {:api_uri => api_uri, :short_uri => short_uri}
76
+ project_attributes[:private_key] = private_key unless private_key.nil?
77
+ {public_key.to_sym => project_attributes}
78
+ end
79
+
80
+ # Searches website for project with the supplied public key.
81
+ #
82
+ # Returns a Project instance if the project exists and is available,
83
+ # to the user making the request.
84
+ #
85
+ # Returns +nil+ in all other cases.
86
+ def self.find(public_key)
87
+ return nil if public_key.nil? || public_key.to_s.empty?
88
+ response = API.get("/projects/#{public_key}")
89
+ case response.code
90
+ when 200
91
+ project = Project.new
92
+ project.reinitialize_from_hash!(response['project'])
93
+ else
94
+ nil
95
+ end
96
+ end
97
+
98
+ # Attempts to claim "ownership" of the project using the API key
99
+ # defined in the "credentials" section of your CodeFumes config
100
+ # file.
101
+ #
102
+ # If you need to claim a project for a key that is not defined in
103
+ # your config file, refer to Claim#create.
104
+ #
105
+ # Returns true if the request is successful.
106
+ #
107
+ # Returns +false+ in all other cases.
108
+ def claim
109
+ Claim.create(self, ConfigFile.api_key)
110
+ end
111
+
112
+ # Attempts to relinquish "ownership" of the project using the API key
113
+ # defined in the "credentials" section of your CodeFumes config
114
+ # file.
115
+ #
116
+ # If you need to relinquish ownership of a project for a key that is
117
+ # not defined in your config file, refer to Claim#delete.
118
+ #
119
+ # Returns true if the request is successful.
120
+ #
121
+ # Returns +false+ in all other cases.
122
+ def release
123
+ Claim.destroy(self, ConfigFile.api_key)
124
+ end
125
+
126
+ # Overrides existing attributes with those supplied in +options+. This
127
+ # simplifies the process of updating an object's state when given a response
128
+ # from the CodeFumes API.
129
+ #
130
+ # Valid options are:
131
+ # * name
132
+ # * public_key
133
+ # * private_key
134
+ # * short_uri
135
+ # * community_uri
136
+ # * api_uri
137
+ # * build_status
138
+ #
139
+ # Returns +self+
140
+ def reinitialize_from_hash!(options = {}) #:nodoc:
141
+ @name = options['name'] || options[:name]
142
+ @public_key = options['public_key'] || options[:public_key]
143
+ @private_key = options['private_key'] || options[:private_key]
144
+ @short_uri = options['short_uri'] || options[:short_uri]
145
+ @community_uri = options['community_uri'] || options[:community_uri]
146
+ @api_uri = options['api_uri'] || options[:api_uri]
147
+ @build_status = options['build_status'] || options[:build_status]
148
+ self
149
+ end
150
+
151
+
152
+ private
153
+ def destroy!
154
+ API.delete("/projects/#{@public_key}", :basic_auth => {:username => @public_key, :password => @private_key})
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,54 @@
1
+ module CodeFumes
2
+ # Module with convenience methods used in 'fumes' command line executable
3
+ module CLIHelpers #nodoc
4
+ def public_keys_specified(options)
5
+ return ConfigFile.public_keys if options[:all]
6
+ [options[:public_key] || SourceControl.new('./').public_key]
7
+ end
8
+
9
+ def print_api_mode_notification
10
+ puts "NOTE: Sending all requests & data to non-production server! (#{API.base_uri})"
11
+ end
12
+
13
+ def issue_project_commands(message, public_keys, &block)
14
+ public_keys.each do |public_key|
15
+ print "#{message}...'#{public_key}': "
16
+ project = Project.find(public_key.to_s)
17
+ yield(project)
18
+ end
19
+
20
+ puts ""
21
+ puts "Done!"
22
+ end
23
+
24
+ def wrap_with_standard_feedback(project, &block)
25
+ if project.nil?
26
+ puts "Project Not Found."
27
+ elsif yield != true
28
+ puts "Denied."
29
+ else
30
+ puts "Success!"
31
+ end
32
+ end
33
+
34
+ def command_doesnt_use_api?(command)
35
+ [:'api-key'].include?(command.name)
36
+ end
37
+
38
+ # lifted from the Github gem
39
+ def has_launchy?(&block)
40
+ begin
41
+ gem 'launchy'
42
+ require 'launchy'
43
+ block.call
44
+ rescue Gem::LoadError
45
+ raise Errors::MissingLaunchyGem, "'launchy' gem required, but missing"
46
+ end
47
+ end
48
+
49
+ # lifted from the Github gem
50
+ def open_in_browser(url, &block)
51
+ has_launchy? {Launchy::Browser.new.visit url}
52
+ end
53
+ end
54
+ end
@@ -67,9 +67,10 @@ module CodeFumes
67
67
 
68
68
  # Returns a Hash representation of a specific project contained in
69
69
  # the CodeFumes config file.
70
- def options_for_project(public_key)
70
+ def options_for_project(project_or_key)
71
+ public_key = project_or_key.is_a?(Project) ? project_or_key.public_key : project_or_key
71
72
  config = serialized
72
- public_key && config[:projects] && config[:projects][public_key.to_sym] || {}
73
+ project_or_key && config[:projects] && config[:projects][public_key.to_sym] || {}
73
74
  end
74
75
 
75
76
  def public_keys
@@ -0,0 +1,21 @@
1
+ module CodeFumes
2
+ module Errors #:nodoc:
3
+ class InsufficientCredentials < StandardError #:nodoc:
4
+ end
5
+
6
+ class UnsupportedScmToolError < StandardError #:nodoc:
7
+ end
8
+
9
+ class UnknownProjectError < StandardError #:nodoc:
10
+ end
11
+
12
+ class NoUserApiKeyError < ArgumentError #:nodoc:
13
+ end
14
+
15
+ class NoApiKeySpecified < ArgumentError #:nodoc:
16
+ end
17
+
18
+ class MissingLaunchyGem < Gem::LoadError #:nodoc:
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,10 @@
1
+ module ExitCodes
2
+ SUCCESS = 0
3
+ UNSUPPORTED_SCM = 1
4
+ PROJECT_NOT_FOUND = 2
5
+ NO_USER_CREDENTIALS = 3
6
+ INCORRECT_USER_CREDENTIALS = 4
7
+ NO_API_KEY_SPECIFIED = 5
8
+ MISSING_DEPENDENCY = 6
9
+ UNKNOWN = 100
10
+ end
@@ -0,0 +1,113 @@
1
+ module CodeFumes
2
+ # Simple class responsible for creating a project on the CodeFumes
3
+ # website, storing project information in the CodeFumes config file,
4
+ # and synchronizing a repository's commit history with CodeFumes.com.
5
+ #
6
+ # NOTE: Technically this can be used by anything (obviously), but it
7
+ # was written with the intention of being used with the
8
+ # +harvest_repo_metrics+ script, and is essentially geared for that
9
+ # scenario.
10
+ class Harvester
11
+ attr_reader :path
12
+ DEFAULT_PATH = './' #:nodoc:
13
+
14
+ # Accepts the following options:
15
+ # * +:path+ - the path of the repository to gather information from
16
+ # (Defaults to './').
17
+ # * +:public_key+ - Sets the public key of the project. This
18
+ # property will be read from the CodeFumes config file if one
19
+ # exists for the repository supplied at +:path+.
20
+ # * +:private_key+ - Sets the private key of the project. This
21
+ # property will be read from the CodeFumes config file if on
22
+ # exists for the repository supplied at +:path+.
23
+ # * +:name+ - Sets the name of the project on the CodeFumes site
24
+ #
25
+ # Note:
26
+ # Neither the +public_key+ nor +private_key+ is supported
27
+ # when creating a new project, but is used when updating an
28
+ # existing one. This prevents custom public keys from being
29
+ # created, but allows the user to share the public/private
30
+ # keys of a project with other users, or use them on other
31
+ # machines.
32
+ def initialize(passed_in_options = {})
33
+ options = passed_in_options.dup
34
+ @path = File.expand_path(options.delete(:path) || DEFAULT_PATH)
35
+ @repository = SourceControl.new(@path)
36
+
37
+ public_key = options[:public_key] || @repository.public_key
38
+ private_key = options[:private_key] || @repository.private_key
39
+ @project = initialize_project(public_key, private_key)
40
+ end
41
+
42
+ # Creates or updates a project information on the CodeFumes site,
43
+ # synchronizes the repository's commit history, and prints the
44
+ # results to STDOUT.
45
+ #
46
+ # Returns a Hash containing the keys :successful_count and :total_count
47
+ # if the process succeeded and updates were posted to the server with
48
+ # their associated values.
49
+ #
50
+ # Returns and empty Hash if the local repository is in sync with the
51
+ # server and no updates were posted.
52
+ #
53
+ # Returns +false+ if the process failed.
54
+ def publish_data!
55
+ if @project.save
56
+ store_public_key_in_repository
57
+ update_codefumes_config_file
58
+ generate_and_save_payload || {}
59
+ else
60
+ false
61
+ end
62
+ end
63
+
64
+ # Returns the CodeFumes public key of the project that is located
65
+ # at the supplied path.
66
+ def public_key
67
+ @project.public_key
68
+ end
69
+
70
+ # Returns the CodeFumes private key of the project that is located
71
+ # at the supplied path.
72
+ def private_key
73
+ @project.private_key
74
+ end
75
+
76
+ # Returns the CodeFumes 'short uri' of the project that is located
77
+ # at the supplied path. The 'short uri' is similar to a Tiny URL for
78
+ # a project.
79
+ def short_uri
80
+ @project.short_uri
81
+ end
82
+
83
+ private
84
+ #TODO: Smarten this up a bit.
85
+ # - Handle nils more cleanly, etc.
86
+ # - Don't #create if _something_ was supplied. Verify it was valid or exit.
87
+ def initialize_project(public_key = nil, private_key = nil)
88
+ return Project.create if public_key.nil? || public_key.empty?
89
+
90
+ msg = "Project public key provided was not found via the API (supplied '#{public_key}')."
91
+ Project.find(public_key) || raise(Errors::UnknownProjectError, msg)
92
+ end
93
+
94
+ def store_public_key_in_repository
95
+ @repository.store_public_key(@project.public_key)
96
+ end
97
+
98
+ def update_codefumes_config_file
99
+ ConfigFile.save_project(@project)
100
+ end
101
+
102
+ def generate_and_save_payload
103
+ payload = @repository.payload(Commit.latest_identifier(@project), "HEAD")
104
+ if payload.empty?
105
+ nil
106
+ else
107
+ payloads = Payload.prepare(@project, payload)
108
+ successful_requests = payloads.select {|payload| payload.save == true}
109
+ {:successful_count => successful_requests.size, :total_count => payloads.size}
110
+ end
111
+ end
112
+ end
113
+ end