codefumes_harvester 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.1.0 / 09-08-09
2
+
3
+ * 1 major enhancement:
4
+ * Initial release
data/Manifest.txt ADDED
@@ -0,0 +1,17 @@
1
+ History.txt
2
+ Manifest.txt
3
+ PostInstall.txt
4
+ README.txt
5
+ Rakefile
6
+ bin/harvest_repo_metrics
7
+ lib/codefumes_harvester.rb
8
+ lib/codefumes_harvester/errors.rb
9
+ lib/codefumes_harvester/harvester.rb
10
+ lib/codefumes_harvester/quick_metric.rb
11
+ lib/codefumes_harvester/source_control.rb
12
+ lib/harvest_repo_metrics/cli.rb
13
+ spec/codometer_harvester/harvester_spec.rb
14
+ spec/codometer_harvester/source_control_spec.rb
15
+ spec/harvest_repo_metrics_cli_spec.rb
16
+ spec/spec_helper.rb
17
+ tasks/codefumes.rake
data/PostInstall.txt ADDED
@@ -0,0 +1,7 @@
1
+
2
+ For more information on codometer-harvester, see http://codometer-harvester.rubyforge.org
3
+
4
+ NOTE: Change this information in PostInstall.txt
5
+ You can also delete it if you don't want it.
6
+
7
+
data/README.txt ADDED
@@ -0,0 +1,114 @@
1
+ = codefumes-harvester
2
+
3
+ * http://codefumes.com
4
+ * http://cf-harvester.rubyforge.org/rdoc
5
+
6
+ == DESCRIPTION:
7
+
8
+ CodeFumesHarvester provides a set of high-level tools for gathering
9
+ history and common metrics from a local repository and sending them to
10
+ CodeFumes.com[http://codefumes.com].
11
+
12
+ == FEATURES/PROBLEMS:
13
+
14
+ === Features
15
+
16
+ * Executable +harvest_repo_metrics+ script which gathers a repository's
17
+ commit lineage
18
+ * Rake tasks for grabbing content from metric_fu (coverage,
19
+ lines of 'code code' vs 'test code' ratio).
20
+ * QuickMetric class which can be used to link a custom attribute to the
21
+ current repository commit identifier of a code base to a CodeFumes
22
+ project. This makes it trivial to send up custom attributes from
23
+ scripts, rake tasks, or even the console, if desired.
24
+
25
+ === Problems / Things to Note
26
+ * The only supported SCM right now is git. We know this sucks. Adding
27
+ in other SCMs shouldn't be hard, and is planned. It's just not done
28
+ yet.
29
+
30
+ == SYNOPSIS:
31
+
32
+ === Associating a repository to a CodeFumes.com project
33
+
34
+ # Use +harvest_repo_metrics+ to create a project on CodeFumes.com
35
+ # In your terminal (not IRB):
36
+
37
+ # If you don't care what the public key of your project is:
38
+ $ harvest_repo_metrics
39
+ => results in a project URI similar to: http://codefumes.com/p/uPnf
40
+
41
+ # If you want to customize the public_key of your project:
42
+ $ harvest_repo_metrics -k my_custom_key
43
+ => results in a project URI of: http://codefumes.com/p/my_custom_key
44
+ (assuming it was not already taken)
45
+
46
+ # If you are on another machine and want to update that project,
47
+ # you can pass both the public & private keys of the project for
48
+ # authentication:
49
+ $ harvest_repo_metrics -k my_custom_key -a some-generated-private-key
50
+
51
+
52
+ === Sending up custom metrics using QuickMetric
53
+
54
+ # Send up your own data and associate it with the current commit
55
+ # (HEAD, in git parlance) of your current directory. This makes the
56
+ # (bold) assumption that the repository is already set up as a CodeFumes
57
+ # project, and that you want to link the data provided to the current
58
+ # commit only (think 'rake task' and 'build server').
59
+ #
60
+ # An IRB example:
61
+ > require 'codefumes_harvester'
62
+ > QuickMetric.save(:name_of_metric => "some value")
63
+ => true # returns +false+ if it failed
64
+ ...that's all
65
+
66
+ === Sending up coverage info from metric_fu output
67
+ # Assuming you have run 'rake metrics:all'
68
+ $ rake cf:metric_fu:coverage
69
+
70
+ === Sending up lines of code code / lines of test code ratio
71
+
72
+ # This is only on Rails projects, I believe
73
+ $ rake cf:metric_fu:loc_lot_stats
74
+
75
+
76
+ == REQUIREMENTS:
77
+
78
+ * +codefumes+ gem
79
+
80
+ == INSTALL:
81
+
82
+ From RubyForge:
83
+
84
+ sudo gem install codefumes-harvester
85
+
86
+ Or, from Github:
87
+
88
+ sudo gem install cosyn-codefumes-harvester
89
+
90
+
91
+ == LICENSE:
92
+
93
+ (The MIT License)
94
+
95
+ Copyright (c) 2009 FIXME full name
96
+
97
+ Permission is hereby granted, free of charge, to any person obtaining
98
+ a copy of this software and associated documentation files (the
99
+ 'Software'), to deal in the Software without restriction, including
100
+ without limitation the rights to use, copy, modify, merge, publish,
101
+ distribute, sublicense, and/or sell copies of the Software, and to
102
+ permit persons to whom the Software is furnished to do so, subject to
103
+ the following conditions:
104
+
105
+ The above copyright notice and this permission notice shall be
106
+ included in all copies or substantial portions of the Software.
107
+
108
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
109
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
110
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
111
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
112
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
113
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
114
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,40 @@
1
+ %w[rubygems rake rake/clean fileutils rubigen hoe].each { |f| require f }
2
+ require File.dirname(__FILE__) + '/lib/codefumes_harvester'
3
+
4
+ begin
5
+ require "hanna/rdoctask"
6
+ rescue LoadError
7
+ require 'rake/rdoctask'
8
+ end
9
+
10
+ # Load in the harvester ane metric_fu gems if available so we can collect metrics
11
+ begin
12
+ require "metric_fu"
13
+ rescue LoadError
14
+ end
15
+
16
+ $hoe = Hoe.spec('codefumes_harvester') do |p|
17
+ p.developer('Cosyn Technologies', 'tom.kersten@cosyntech.com')
18
+ p.rubyforge_name = 'codefumes'
19
+ p.extra_deps = [
20
+ ['codefumes','>= 0.1.0'],
21
+ ['mojombo-grit','>= 1.1.1']
22
+ ]
23
+ p.extra_dev_deps = [
24
+ ['mojombo-chronic', ">= 0.3.0"],
25
+ ['jscruggs-metric_fu', ">= 1.1.5"]
26
+ ]
27
+ p.clean_globs |= %w[**/.DS_Store tmp *.log]
28
+ #path = (p.rubyforge_name == p.name) ? p.rubyforge_name : "\#{p.rubyforge_name}/\#{p.name}"
29
+ #p.remote_rdoc_dir = File.join(path.gsub(/^#{p.rubyforge_name}\/?/,''), 'rdoc')
30
+ p.remote_rdoc_dir = ''
31
+ p.rsync_args = '-av --delete --ignore-errors'
32
+ end
33
+
34
+ # load /tasks/*.rake, except for the codefumes.rake file, that will be
35
+ # done in lib/codefumes_harvester.rb (the task_defined? method didn't seem
36
+ # to catch it)
37
+ # TODO: Find out what is/was being done wrong that this is necessary
38
+ Dir['tasks/**/*.rake'].each { |t| load t unless t[/codefumes.rake/]}
39
+
40
+ task :default => [:spec]
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Created on 2009-9-2.
4
+ # Copyright (c) 2009. All rights reserved.
5
+
6
+ require File.expand_path(File.dirname(__FILE__) + "/../lib/codefumes_harvester")
7
+
8
+ require "harvest_repo_metrics/cli"
9
+
10
+ HarvestRepoMetric::CLI.execute(STDOUT, ARGV)
@@ -0,0 +1,22 @@
1
+ require 'grit'
2
+ require 'rake'
3
+ require 'codefumes'
4
+ require 'codefumes_harvester/harvester'
5
+ require 'codefumes_harvester/source_control'
6
+ require 'codefumes_harvester/errors'
7
+ require 'codefumes_harvester/quick_metric'
8
+
9
+ include CodeFumesHarvester
10
+
11
+ module CodeFumesHarvester
12
+ VERSION = '0.1.0'
13
+ LIB_ROOT = File.dirname(__FILE__)
14
+ end
15
+
16
+ # Shamelessly borrowed from metric_fu setup (http://metric-fu.rubyforge.org/)
17
+ unless Rake::Task.task_defined? "cf:store_repo_metrics"
18
+ # Load the rakefile so users of the gem get the default cf tasks
19
+ # This makes a rash assumption that if you have the 'store_repo_metrics'
20
+ # task, all others will be defined...and that if you don't, you want them all.
21
+ load File.join(CodeFumesHarvester::LIB_ROOT, '..', 'tasks', 'codefumes.rake')
22
+ end
@@ -0,0 +1,6 @@
1
+ module CodeFumesHarvester
2
+ module Errors #:nodoc:
3
+ class UnsupportedScmToolError < StandardError #:nodoc:
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,110 @@
1
+ module CodeFumesHarvester
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 on
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
+ def initialize(passed_in_options = {})
25
+ options = passed_in_options.dup
26
+ @path = File.expand_path(options.delete(:path) || DEFAULT_PATH)
27
+ @repository = initialize_repository
28
+ options.merge!(:public_key => options[:public_key] || @repository.public_key)
29
+ options.merge!(:private_key => options[:private_key] || @repository.private_key)
30
+ @project = initialize_project(options)
31
+ end
32
+
33
+ # Creates or updates a project information on the CodeFumes site,
34
+ # synchronizes the repository's commit history, and prints the
35
+ # results to STDOUT.
36
+ #
37
+ # Returns +true+ if the process succeeded.
38
+ #
39
+ # Returns +false+ if the process failed.
40
+ def publish_data!
41
+ if @project.save
42
+ store_public_key_in_repository
43
+ update_codefumes_config_file
44
+ payload_results = generate_and_save_payload
45
+ if payload_results.nil?
46
+ puts "Local repository is in sync with server. No updates posted."
47
+ else
48
+ puts "Successfully saved #{payload_results[:successful_count]} of #{payload_results[:total_count]} payloads."
49
+ end
50
+ puts "Exiting."
51
+ puts
52
+ true
53
+ else
54
+ false
55
+ end
56
+ end
57
+
58
+ # Returns the CodeFumes public key of the project that is located
59
+ # at the supplied path.
60
+ def public_key
61
+ @project.public_key
62
+ end
63
+
64
+ # Returns the CodeFumes private key of the project that is located
65
+ # at the supplied path.
66
+ def private_key
67
+ @project.private_key
68
+ end
69
+
70
+ # Returns the CodeFumes 'short uri' of the project that is located
71
+ # at the supplied path. The 'short uri' is similar to a Tiny URL for
72
+ # a project.
73
+ def short_uri
74
+ @project.short_uri
75
+ end
76
+
77
+ private
78
+ def initialize_repository
79
+ begin
80
+ SourceControl.new(@path)
81
+ rescue Grit::InvalidGitRepositoryError
82
+ raise Errors::UnsupportedScmToolError
83
+ end
84
+ end
85
+
86
+ def initialize_project(options = {})
87
+ options = CodeFumes::ConfigFile.options_for_project(options[:public_key]).merge(options)
88
+ CodeFumes::Project.new(options)
89
+ end
90
+
91
+ def store_public_key_in_repository
92
+ @repository.store_public_key(@project.public_key)
93
+ end
94
+
95
+ def update_codefumes_config_file
96
+ CodeFumes::ConfigFile.save_project(@project)
97
+ end
98
+
99
+ def generate_and_save_payload
100
+ payload = @repository.payload(CodeFumes::Commit.latest_identifier(public_key), "HEAD")
101
+ if payload.empty?
102
+ nil
103
+ else
104
+ payloads = CodeFumes::Payload.prepare(:public_key => public_key, :private_key => private_key, :content => payload)
105
+ successful_requests = payloads.select {|payload| payload.save == true}
106
+ {:successful_count => successful_requests.size, :total_count => payloads.size}
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,19 @@
1
+ module CodeFumesHarvester
2
+ class QuickMetric
3
+ # Associates a Hash of +custom_attributes+ with the current commit
4
+ # identifier of the repository located at +repository_path+.
5
+ #
6
+ # Returns +true+ if the request succeeded.
7
+ #
8
+ # Returns +false+ if the request failed.
9
+ def self.save(custom_attributes, repository_path = './')
10
+ repo = SourceControl.new(repository_path)
11
+ commit = {:identifier => repo.local_revision_identifier,
12
+ :custom_attributes => custom_attributes
13
+ }
14
+ content = {:commits => [commit]}
15
+ payload_set = CodeFumes::Payload.prepare(:public_key => repo.public_key, :private_key => repo.private_key, :content => content)
16
+ payload_set.reject {|payload| payload.save}.empty?
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,132 @@
1
+ module CodeFumesHarvester
2
+ # Defines the contract between CodeFumes and any local source control
3
+ # management system (SCM).
4
+ #
5
+ # *NOTE:* Git is currently the only supported SCM. We look
6
+ # forward to changing this soon.
7
+ class SourceControl
8
+ SUPPORTED_SCMS_AND_DETECTORS = {:git => '.git'} #:nodoc:
9
+
10
+ # Sets up a SourceControl object to read content from the repository
11
+ # located at +path+.
12
+ def initialize(path)
13
+ @repository = Grit::Repo.new(path)
14
+ end
15
+
16
+ # Returns a serialized Hash containing a single +:commits+ key
17
+ # associated with an Array of serialized commit information, ready
18
+ # to be sent to the CodeFumes service.
19
+ def payload_between(from = initial_commit_identifier, to = "HEAD")
20
+ start_commit = from || initial_commit_identifier
21
+ end_commit = to || "HEAD"
22
+ new_commits = commits_between(start_commit, end_commit)
23
+ new_commits.empty? ? {} : {:commits => new_commits}
24
+ end
25
+ alias :payload :payload_between
26
+
27
+ # Returns the first commit identifier of a repository's history.
28
+ def initial_commit_identifier
29
+ initial_commit.sha
30
+ end
31
+
32
+ # Returns an array of 'symbolized' executable names for all
33
+ # supported SCMs.
34
+ #
35
+ # The names are returned as symbols.
36
+ def self.supported_systems
37
+ SUPPORTED_SCMS_AND_DETECTORS.keys
38
+ end
39
+
40
+ # Accepts command-line executable name of SCM and returns whether it
41
+ # is a supported SCM or not. +tool_cli_name+ should be the
42
+ # name of the executable, not the 'full name' of the application
43
+ # (ex: 'svn' not 'subversion').
44
+ #
45
+ # Returns +true+ if the SCM is supported
46
+ #
47
+ # Returns +false+ if the SCM is not supported.
48
+ def self.supported_system?(tool_cli_name)
49
+ SUPPORTED_SCMS_AND_DETECTORS.keys.include?(tool_cli_name.to_sym)
50
+ end
51
+
52
+ # Stores the public_key of the project associated with the
53
+ # underlying local repository. This will not be necessary
54
+ # with all SCMs.
55
+ #
56
+ # For example, in a git repository, this method will store a
57
+ # +codefumes+ key in the +.git/config+ file. This value can be used
58
+ # as a lookup key for other tools to use in conjunction with the
59
+ # CodeFumes config file (see CodeFumes::Config) to interact with a
60
+ # CodeFumes project.
61
+ def store_public_key(public_key)
62
+ @repository.config["codefumes.public-key"] = public_key
63
+ end
64
+
65
+ # Removes any association to the CodeFumes service which would have
66
+ # been added using the +store_public_key+ method.
67
+ #
68
+ # This method does not remove anything from the user's global
69
+ # CodeFumes config file.
70
+ def unlink_from_codefumes!
71
+ @repository.git.config({}, "--remove-section", "codefumes")
72
+ end
73
+
74
+ # Returns the public key of the project associated with this
75
+ # repository.
76
+ def public_key
77
+ @repository.config["codefumes.public-key"]
78
+ end
79
+
80
+ # Returns the private key of the project assciated with this
81
+ # repository.
82
+ def private_key
83
+ CodeFumes::ConfigFile.options_for_project(public_key)[:private_key]
84
+ end
85
+
86
+ # Returns the current revision identifier of the underlying
87
+ # repository ('HEAD' of the supplied branch in git parlance).
88
+ def local_revision_identifier(branch_name = "master")
89
+ raise ArgumentError, "nil branch name supplied" if branch_name.nil?
90
+ @repository.get_head(branch_name).commit.sha
91
+ end
92
+
93
+ # Returns the full path of the underlying repository.
94
+ def path
95
+ @repository.path
96
+ end
97
+
98
+ private
99
+ def initial_commit
100
+ @repository.log.last
101
+ end
102
+
103
+ # Returns Array of serialized commit data. Each item in the Array
104
+ # contains attributes of a single commit.
105
+ def commits_between(from, to, including_from_commit = false)
106
+ commit_list = @repository.commits_between(from,to)
107
+
108
+ if including_from_commit == true || from == initial_commit_identifier
109
+ commit_list = [initial_commit] + commit_list
110
+ end
111
+
112
+ commit_list.map do |commit|
113
+ {
114
+ :identifier => commit.sha,
115
+ :author_name => commit.author.name,
116
+ :author_email => commit.author.email,
117
+ :committer_name => commit.committer.name,
118
+ :committer_email => commit.committer.email,
119
+ :authored_at => commit.authored_date,
120
+ :committed_at => commit.committed_date,
121
+ :message => commit.message,
122
+ :short_message => commit.short_message,
123
+ :parent_identifiers => commit.parents.map(&:sha).join(','),
124
+ :line_additions => commit.stats.additions,
125
+ :line_deletions => commit.stats.deletions,
126
+ :line_total => commit.stats.deletions,
127
+ :affected_file_count => commit.stats.files.count
128
+ }
129
+ end.reverse
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,63 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless
2
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
+
4
+ require 'optparse'
5
+
6
+ module HarvestRepoMetric
7
+ # Class by the +harvest_metric+ script for command line parsing and
8
+ # delegation to the Harvester class.
9
+ class CLI
10
+ include CodeFumesHarvester
11
+
12
+ # Parses command-line options passed into +harvest_repo_metrics+
13
+ # and sends data collected to CodeFumes website.
14
+ def self.execute(stdout, arguments=[])
15
+ parse_command_line_options!(stdout, arguments)
16
+ harvester = Harvester.new(@options)
17
+
18
+ if harvester.publish_data!
19
+ stdout.puts "Project saved to CodeFumes.com. Visit #{harvester.short_uri}"
20
+ else
21
+ stdout.puts "Error saving project...please try again or file a bug report."
22
+ exit(1)
23
+ end
24
+ end
25
+
26
+ private
27
+ def self.parse_command_line_options!(stdout, cl_arguments)
28
+ @options = {}
29
+ begin
30
+ OptionParser.new do |opts|
31
+ opts.banner = <<-BANNER.gsub(/^ /,'')
32
+ Tool used to upload code & repository statistics to
33
+ the CodeFumes website.
34
+
35
+ Usage: #{File.basename($0)} [options]
36
+
37
+ Options are:
38
+ BANNER
39
+ opts.separator ""
40
+ opts.on("-k", "--project_key [PROJECT_KEY]",
41
+ "The public CodeFumes API key for this project."
42
+ ) {|public_key| @options[:public_key] = public_key}
43
+ opts.on("-a", "--access_key [PROJECT_PRIVATE_KEY]",
44
+ "The private CodeFumes API access key for this project."
45
+ ) {|private_key| @options[:private_key] = private_key}
46
+ opts.on("-p", "--path [REPOSITORY_PATH]",
47
+ "The local path to the directory containing this project's code."
48
+ ) {|path| @options[:path] = Dir.new(File.expand_path(path)).path}
49
+ opts.on("-h", "--help",
50
+ "Show this help message."
51
+ ) { stdout.puts opts; exit }
52
+ opts.on("-n", "--name [PROJECT_NAME]",
53
+ "Name of project."
54
+ ) {|name| @options[:name] = name}
55
+ opts.parse!(cl_arguments)
56
+ end
57
+ rescue Errno::ENOTDIR
58
+ stdout.puts "Path supplied is not a directory. Please supply a path to a directory containing your project."
59
+ exit
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,71 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe Harvester do
4
+ before(:each) do
5
+ this_files_dir = File.dirname(__FILE__)
6
+ @no_scm_path = "#{this_files_dir}/../fixtures/sample_project_dirs/no_scm"
7
+ @git_scm_path = "#{this_files_dir}/../fixtures/sample_project_dirs/git_repository"
8
+ end
9
+
10
+ describe "initialization" do
11
+ context "with a path to a directory that is using the git SCM tool" do
12
+ before(:each) do
13
+ @repository = Grit::Repo.new(@git_scm_path)
14
+ @project = CodeFumes::Project.new
15
+ Grit::Repo.stub!(:new).with(File.expand_path(@git_scm_path)).and_return(@repository)
16
+ CodeFumes::Project.stub!(:new).and_return(@project)
17
+ end
18
+
19
+ def initialize_with_git
20
+ Harvester.new(:path => @git_scm_path)
21
+ end
22
+
23
+ it "creates a new Grit::Repo instance" do
24
+ Grit::Repo.should_receive(:new).with(File.expand_path(@git_scm_path)).and_return(@repository)
25
+ initialize_with_git
26
+ end
27
+
28
+ it "creates a new CodeFumes::Project instance" do
29
+ CodeFumes::Project.should_receive(:new).and_return(@project)
30
+ initialize_with_git
31
+ end
32
+ end
33
+
34
+ context "with a path to a directory that is not using a supported SCM" do
35
+ it "raises an error" do
36
+ lambda {Harvester.new(:path => @no_scm_path)}.should raise_error(Errors::UnsupportedScmToolError)
37
+ end
38
+ end
39
+ end
40
+
41
+ describe "path" do
42
+ it "returns the full path to a project" do
43
+ harvester = Harvester.new(:path => @git_scm_path)
44
+ harvester.path.should == File.expand_path(@git_scm_path)
45
+ end
46
+ end
47
+
48
+ describe "publish_data!" do
49
+ before(:each) do
50
+ @harvester = Harvester.new(:path => @git_scm_path)
51
+ @payload = mock("CodeFumes::Payload instance", :save => true)
52
+ CodeFumes::Payload.stub!(:new).and_return(@payload)
53
+ end
54
+
55
+ it "initializes a new Payload object" do
56
+ payload_params = {:public_key => anything(), :private_key => anything(), :content => anything()}
57
+ CodeFumes::Payload.should_receive(:new).with(payload_params).and_return(@payload)
58
+ @harvester.publish_data!
59
+ end
60
+
61
+ it "saves the Payload to the website" do
62
+ @payload.should_receive(:save).and_return(true)
63
+ @harvester.publish_data!
64
+ end
65
+
66
+ it "updates the config file's project information" do
67
+ CodeFumes::ConfigFile.should_receive(:save_project)
68
+ @harvester.publish_data!
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,199 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe SourceControl do
4
+ after(:each) do
5
+ SourceControl.new(GIT_FIXTURE_REPO_PATH).unlink_from_codefumes!
6
+ unless CodeFumes::ConfigFile.path == File.expand_path('~/.codefumes_config')
7
+ File.delete(CodeFumes::ConfigFile.path) if File.exists?(CodeFumes::ConfigFile.path)
8
+ end
9
+ end
10
+
11
+ describe "creation" do
12
+ context "with a path to a directory that is not a git repository" do
13
+ it "raises an error" do
14
+ invalid_path = File.dirname(__FILE__)
15
+ lambda {SourceControl.new(invalid_path)}.should raise_error(Grit::InvalidGitRepositoryError)
16
+ end
17
+ end
18
+ end
19
+
20
+ describe "supported_systems" do
21
+ it "includes :git" do
22
+ SourceControl.supported_systems.should include(:git)
23
+ end
24
+ end
25
+
26
+ describe "supported_system?" do
27
+ SourceControl.supported_systems.each do |scm_tool|
28
+ it "returns true for the string '#{scm_tool.to_s}'" do
29
+ SourceControl.supported_system?(scm_tool.to_s)
30
+ end
31
+
32
+ it "returns true for the symbol ':#{scm_tool.to_s}'" do
33
+ SourceControl.supported_system?(scm_tool.to_sym)
34
+ end
35
+ end
36
+ end
37
+
38
+ describe "initial_commit_identifier" do
39
+ before(:each) do
40
+ git_repo_path = File.expand_path(File.dirname(__FILE__) + '/../fixtures/sample_project_dirs/git_repository')
41
+ @repository = SourceControl.new(git_repo_path)
42
+ end
43
+
44
+ it "returns the identifer (sha/revision #) of the first commit" do
45
+ @repository.initial_commit_identifier.should == "a8b3e73fc5e4bc46bbdf5c1cab38cb2ce47ba2d0"
46
+ end
47
+ end
48
+
49
+ describe "payload_between" do
50
+ before(:each) do
51
+ git_repo_path = File.expand_path(File.dirname(__FILE__) + '/../fixtures/sample_project_dirs/git_repository')
52
+ @repository = SourceControl.new(git_repo_path)
53
+ end
54
+
55
+ it "returns a Hash containing the key ':commits'" do
56
+ @repository.payload_between.keys.should include(:commits)
57
+ end
58
+
59
+ it "is aliased to 'payload'" do
60
+ @repository.payload_between.keys.should == @repository.payload.keys
61
+ # FIXME: Improve this...not a complete test. Object ids getting in the way
62
+ @repository.payload_between[:commits].each_with_index do |commit, index|
63
+ commit[:identifier].should == @repository.payload[:commits][index][:identifier]
64
+ end
65
+ end
66
+
67
+ it "returns the commits in the same order a standard 'log view' of the repository would" do
68
+ first_commit = @repository.payload_between[:commits].last
69
+ first_commit[:identifier].should == "a8b3e73fc5e4bc46bbdf5c1cab38cb2ce47ba2d0"
70
+ end
71
+
72
+ context "the ':commits' key returned" do
73
+ context "contains a list of data for each commit" do
74
+ before(:each) do
75
+ @first_commit = @repository.payload_between[:commits].last
76
+ end
77
+
78
+ it "contains 'id', which is the 'sha' of the commit" do
79
+ @first_commit[:identifier].should == "a8b3e73fc5e4bc46bbdf5c1cab38cb2ce47ba2d0"
80
+ end
81
+
82
+ it "contains 'author', which points to the 'email' & the 'name' of the commit (in a Hash)" do
83
+ @first_commit[:author_email].should == "tkersten@obtiva.com"
84
+ @first_commit[:author_name].should == "Tom Kersten"
85
+ end
86
+
87
+ it "contains 'committer', which points to the 'email' & the 'name' of the commit (in a Hash)" do
88
+ @first_commit[:committer_email].should == "tkersten@obtiva.com"
89
+ @first_commit[:committer_name].should == "Tom Kersten"
90
+ end
91
+
92
+ it "contains 'message', which holds the full message of the commit" do
93
+ @first_commit[:message].should == "Initial commit with description of directory"
94
+ end
95
+
96
+ it "contains 'short_message', which holds the first line of the message of the commit" do
97
+ @first_commit[:short_message].should == "Initial commit with description of directory"
98
+ end
99
+
100
+ it "contains 'committed_date' of the commit" do
101
+ @first_commit[:committed_at].should == Chronic.parse("Sat May 09 08:52:14 -0500 2009")
102
+ end
103
+
104
+ it "contains 'authored_date' of the commit" do
105
+ @first_commit[:authored_at].should == Chronic.parse("Sat May 09 08:52:14 -0500 2009")
106
+ end
107
+
108
+ it "contains 'parent_commits' of the commit" do
109
+ @first_commit[:parent_identifiers].should_not be_nil
110
+ end
111
+
112
+ it "contains a 'line_additions' key" do
113
+ @first_commit[:line_additions].should_not be_nil
114
+ end
115
+
116
+ it "contains an 'line_deletions' key" do
117
+ @first_commit[:line_deletions].should_not be_nil
118
+ end
119
+
120
+ it "contains an 'line_total' key" do
121
+ @first_commit[:line_total].should_not be_nil
122
+ end
123
+
124
+ it "contains a 'affected_file_count' key" do
125
+ @first_commit[:affected_file_count].should_not be_nil
126
+ end
127
+ end
128
+ end
129
+
130
+ context "when 'from' is specified but the value is nil" do
131
+ it "defaults to the initial commit identifier of the repository" do
132
+ @repository.should_receive(:commits_between).with("a8b3e73fc5e4bc46bbdf5c1cab38cb2ce47ba2d0", anything()).and_return([])
133
+ @repository.payload_between(nil, "something")
134
+ end
135
+ end
136
+
137
+ context "when 'to' is specified but the value is nil" do
138
+ it "defaults to the 'HEAD' commit identifier of the repository" do
139
+ @repository.should_receive(:commits_between).with(anything(), "HEAD").and_return([])
140
+ @repository.payload_between("something", nil)
141
+ end
142
+ end
143
+
144
+ context "when there is no new information to send up" do
145
+ it "returns an empty object" do
146
+ @repository.payload_between("HEAD", "HEAD").should be_empty
147
+ end
148
+ end
149
+ end
150
+
151
+ describe "public_key=" do
152
+ before(:each) do
153
+ git_repo_path = File.expand_path(File.dirname(__FILE__) + '/../fixtures/sample_project_dirs/git_repository')
154
+ @public_key = "public_key_specified"
155
+ @repository = SourceControl.new(git_repo_path)
156
+ @project = CodeFumes::Project.new(:public_key => @public_key)
157
+ end
158
+
159
+ it "stores the supplied public key in the SCM tool's repository-specific configuration" do
160
+ @repository.store_public_key(@public_key)
161
+ @repository.public_key.should == @public_key
162
+ end
163
+ end
164
+
165
+ describe "public_key" do
166
+ before(:each) do
167
+ @public_key = "original_value"
168
+ git_repo_path = File.expand_path(File.dirname(__FILE__) + '/../fixtures/sample_project_dirs/git_repository')
169
+ @repository = SourceControl.new(git_repo_path)
170
+ @repository.store_public_key(@public_key)
171
+ end
172
+
173
+ it "returns the current value of 'codometer.public_key' from the repository-specific configuration" do
174
+ @repository.public_key.should == @public_key
175
+ end
176
+ end
177
+
178
+ describe "local_revision_identifier" do
179
+ before(:each) do
180
+ git_repo_path = File.expand_path(File.dirname(__FILE__) + '/../fixtures/sample_project_dirs/git_repository')
181
+ @repository = SourceControl.new(git_repo_path)
182
+ end
183
+
184
+ it "returns the current revision identifier for the local repository" do
185
+ @repository.local_revision_identifier.should == "7dc0e73fea4625204b7c1e6a48e9a57025be4d7e"
186
+ end
187
+ end
188
+
189
+ describe "path" do
190
+ before(:each) do
191
+ @git_repo_path = File.expand_path(File.dirname(__FILE__) + '/../fixtures/sample_project_dirs/git_repository')
192
+ @repository = SourceControl.new(@git_repo_path)
193
+ end
194
+
195
+ it "returns the full path to the .git directory of the repository" do
196
+ @repository.path.should == @git_repo_path + '/.git'
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,26 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+ require 'harvest_repo_metrics/cli'
3
+
4
+ describe HarvestRepoMetric::CLI, "execute" do
5
+ before(:each) do
6
+ @stdout_io = StringIO.new
7
+ HarvestRepoMetric::CLI.execute(@stdout_io, ["-p", GIT_FIXTURE_REPO_PATH])
8
+ @stdout_io.rewind
9
+ @stdout = @stdout_io.read
10
+ end
11
+
12
+ after(:each) do
13
+ unless CodeFumes::ConfigFile.path == File.expand_path('~/.codefumes_config')
14
+ File.delete(CodeFumes::ConfigFile.path)
15
+ SourceControl.new(GIT_FIXTURE_REPO_PATH).unlink_from_codefumes!
16
+ end
17
+ end
18
+
19
+ it "prints out the path of the project" do
20
+ @stdout.should =~ /Project saved/
21
+ end
22
+
23
+ it "creates a .codefumes_config file" do
24
+ File.exist?(CodeFumes::ConfigFile.path).should be_true
25
+ end
26
+ end
@@ -0,0 +1,19 @@
1
+ require 'rubygems'
2
+
3
+ gem 'ruby-debug'
4
+ require 'ruby-debug'
5
+ require 'chronic'
6
+
7
+ begin
8
+ require 'spec/autorun'
9
+ rescue LoadError
10
+ gem 'rspec'
11
+ require 'spec/autorun'
12
+ end
13
+
14
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
15
+ require 'codefumes_harvester'
16
+
17
+ GIT_FIXTURE_REPO_PATH = File.expand_path(File.dirname(__FILE__) + "/fixtures/sample_project_dirs/git_repository")
18
+ ENV['CODEFUMES_CONFIG_FILE'] = File.expand_path(File.dirname(__FILE__) + '/sample_codefumes_config.tmp')
19
+ CodeFumes::API.mode(:test)
@@ -0,0 +1,66 @@
1
+ namespace :cf do
2
+ desc "Send scm metrics to Codefumes.com"
3
+ task :store_repo_metrics, [:public_key, :private_key] do |tsk, args|
4
+ harvest_repo_metrics_executable = File.expand_path(File.dirname(__FILE__) + "/../bin/harvest_repo_metrics")
5
+ options = args.public_key.nil? ? "" : "-k #{args.public_key}"
6
+ options += args.private_key.nil? ? "" : " -a #{args.private_key}"
7
+ sh "#{harvest_repo_metrics_executable} #{options}"
8
+ end
9
+
10
+
11
+ namespace :metric_fu do
12
+ desc "Collect commit's overall code coverage statistic from metric_fu output"
13
+ task :coverage do |tsk, args|
14
+ err_msg = "No 'rcov' section found in '#{file_path}'"
15
+ puts rcov_section.nil? ? err_msg : save(:coverage => rcov_section[:global_percent_run])
16
+ end
17
+
18
+ desc "Collect current commit's \"lines of code\" & \"lines of test\" metrics from metric_fu output"
19
+ task :loc_lot_stats do |tsk, args|
20
+ if stats_section.nil?
21
+ puts "No 'line stats' section found in '#{file_path}'"
22
+ else
23
+ metrics = {:code_lines_of_code => stats_section[:codeLOC],
24
+ :test_lines_of_code => stats_section[:testLOC],
25
+ :code_to_test_ratio => "1:#{stats_section[:code_to_test_ratio]}"
26
+ }
27
+ puts save(metrics)
28
+ end
29
+ end
30
+
31
+ def file_path
32
+ @file_path ||= File.expand_path('tmp/metric_fu/report.yml')
33
+ end
34
+
35
+ def marshalled_mf_report
36
+ return @marshalled_content unless @marshalled_content.nil?
37
+
38
+ unless File.exists?(file_path)
39
+ raise "No report.yml file. Has 'rake metrics:all' been run yet?"
40
+ end
41
+ @marshalled_content = File.open(file_path) {|f| YAML.load(f)}
42
+ end
43
+
44
+ def rcov_section
45
+ @rcov_section ||= marshalled_mf_report[:rcov]
46
+ end
47
+
48
+ def stats_section
49
+ @stats_section ||= marshalled_mf_report[:stats]
50
+ end
51
+
52
+ def save(custom_attributes_hash)
53
+ nil_value_attributes = custom_attributes_hash.select {|name, value| value.nil?}
54
+ unless nil_value_attributes.empty?
55
+ return "No value supplied for the attributes: #{nil_value_attributes.keys.join(', ')}"
56
+ end
57
+
58
+ metric_names = custom_attributes_hash.keys.join(', ')
59
+ if QuickMetric.save(custom_attributes_hash)
60
+ "Successfully saved metric(s): #{metric_names}"
61
+ else
62
+ "Error saving metric(s): #{metric_names}"
63
+ end
64
+ end
65
+ end
66
+ end
metadata ADDED
@@ -0,0 +1,123 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: codefumes_harvester
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Cosyn Technologies
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-09-08 00:00:00 -05:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: codefumes
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 0.1.0
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: mojombo-grit
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 1.1.1
34
+ version:
35
+ - !ruby/object:Gem::Dependency
36
+ name: mojombo-chronic
37
+ type: :development
38
+ version_requirement:
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: 0.3.0
44
+ version:
45
+ - !ruby/object:Gem::Dependency
46
+ name: jscruggs-metric_fu
47
+ type: :development
48
+ version_requirement:
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: 1.1.5
54
+ version:
55
+ - !ruby/object:Gem::Dependency
56
+ name: hoe
57
+ type: :development
58
+ version_requirement:
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: 2.3.2
64
+ version:
65
+ description: CodeFumesHarvester provides a set of high-level tools for gathering history and common metrics from a local repository and sending them to CodeFumes.com[http://codefumes.com].
66
+ email:
67
+ - tom.kersten@cosyntech.com
68
+ executables:
69
+ - harvest_repo_metrics
70
+ extensions: []
71
+
72
+ extra_rdoc_files:
73
+ - History.txt
74
+ - Manifest.txt
75
+ - PostInstall.txt
76
+ - README.txt
77
+ files:
78
+ - History.txt
79
+ - Manifest.txt
80
+ - PostInstall.txt
81
+ - README.txt
82
+ - Rakefile
83
+ - bin/harvest_repo_metrics
84
+ - lib/codefumes_harvester.rb
85
+ - lib/codefumes_harvester/errors.rb
86
+ - lib/codefumes_harvester/harvester.rb
87
+ - lib/codefumes_harvester/quick_metric.rb
88
+ - lib/codefumes_harvester/source_control.rb
89
+ - lib/harvest_repo_metrics/cli.rb
90
+ - spec/codometer_harvester/harvester_spec.rb
91
+ - spec/codometer_harvester/source_control_spec.rb
92
+ - spec/harvest_repo_metrics_cli_spec.rb
93
+ - spec/spec_helper.rb
94
+ - tasks/codefumes.rake
95
+ has_rdoc: true
96
+ homepage: http://codefumes.com
97
+ post_install_message:
98
+ rdoc_options:
99
+ - --main
100
+ - README.txt
101
+ require_paths:
102
+ - lib
103
+ required_ruby_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: "0"
108
+ version:
109
+ required_rubygems_version: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ version: "0"
114
+ version:
115
+ requirements: []
116
+
117
+ rubyforge_project: codefumes
118
+ rubygems_version: 1.3.1
119
+ signing_key:
120
+ specification_version: 2
121
+ summary: CodeFumesHarvester provides a set of high-level tools for gathering history and common metrics from a local repository and sending them to CodeFumes.com[http://codefumes.com].
122
+ test_files: []
123
+