gitolemy 0.0.4
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.
- checksums.yaml +7 -0
- data/bin/conglomerate.rb +145 -0
- data/bin/serve.rb +59 -0
- data/lib/cache.rb +92 -0
- data/lib/commit.rb +139 -0
- data/lib/commit_stats.rb +225 -0
- data/lib/diff.rb +65 -0
- data/lib/file_diff.rb +98 -0
- data/lib/file_helper.rb +58 -0
- data/lib/file_manager.rb +116 -0
- data/lib/function_trace/c_syntax_tracer.rb +111 -0
- data/lib/function_trace/python_tracer.rb +103 -0
- data/lib/function_trace/ruby_tracer.rb +64 -0
- data/lib/function_trace/tracer.rb +44 -0
- data/lib/gitolemy.rb +1 -0
- data/lib/integrations/airbrake_client.rb +134 -0
- data/lib/integrations/code_climate_client.rb +79 -0
- data/lib/integrations/covhura_client.rb +38 -0
- data/lib/integrations/error_client.rb +55 -0
- data/lib/integrations/git_client.rb +183 -0
- data/lib/integrations/jira_client.rb +145 -0
- data/lib/integrations/rollbar_client.rb +147 -0
- data/lib/line.rb +124 -0
- data/lib/line_tracker.rb +90 -0
- data/lib/loggr.rb +24 -0
- data/lib/notifier.rb +20 -0
- data/lib/project_cache.rb +13 -0
- data/lib/risk_analyzer.rb +53 -0
- data/lib/secure_file_store.rb +61 -0
- data/lib/source_tree.rb +23 -0
- data/lib/stack_tracer.rb +197 -0
- data/lib/store.rb +96 -0
- data/lib/util.rb +10 -0
- data/lib/virtual_file.rb +218 -0
- data/lib/virtual_file_system.rb +78 -0
- data/lib/virtual_function.rb +38 -0
- data/lib/virtual_tree.rb +233 -0
- metadata +223 -0
    
        checksums.yaml
    ADDED
    
    | @@ -0,0 +1,7 @@ | |
| 1 | 
            +
            ---
         | 
| 2 | 
            +
            SHA1:
         | 
| 3 | 
            +
              metadata.gz: '083b930c641ed21a49484825dcf23d74097bb07b'
         | 
| 4 | 
            +
              data.tar.gz: c89bc22ac8076d07a01e7d95b86be3c0924aaef8
         | 
| 5 | 
            +
            SHA512:
         | 
| 6 | 
            +
              metadata.gz: 88c97b2e09ae85864329201c1f91732c2c22aeb49bd25162e3556a8e1b751268c60260ea82a0e3abf3c3b7fcd2b252fefce0d81dcf0282fbfb431e327701cc71
         | 
| 7 | 
            +
              data.tar.gz: 84cdb5564782d60da58bd49b12089edb283765e65e8e93ae6d5ed3b0204c62e026099798c3a3711d264ba38a0a8066bcb8145d9ab2cf75ea4274eef0650accd5
         | 
    
        data/bin/conglomerate.rb
    ADDED
    
    | @@ -0,0 +1,145 @@ | |
| 1 | 
            +
            #! /usr/bin/env ruby
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "rubygems"
         | 
| 4 | 
            +
            require "json"
         | 
| 5 | 
            +
            require "date"
         | 
| 6 | 
            +
            require "optparse"
         | 
| 7 | 
            +
            require "dotenv"
         | 
| 8 | 
            +
            require "active_support/core_ext/string"
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            require "rollbar"
         | 
| 11 | 
            +
            require "airbrake-ruby"
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            require_relative "../lib/file_manager"
         | 
| 14 | 
            +
            require_relative "../lib/notifier"
         | 
| 15 | 
            +
            require_relative "../lib/risk_analyzer"
         | 
| 16 | 
            +
            require_relative "../lib/secure_file_store"
         | 
| 17 | 
            +
             | 
| 18 | 
            +
             | 
| 19 | 
            +
            Dotenv.load()
         | 
| 20 | 
            +
             | 
| 21 | 
            +
            def merge_files_hash!(output, file_hash)
         | 
| 22 | 
            +
              changes = output[:summary][:file_changes]
         | 
| 23 | 
            +
              file_hash.each do |key, value|
         | 
| 24 | 
            +
                changes[key] ||= {}
         | 
| 25 | 
            +
                changes[key].merge!(value)
         | 
| 26 | 
            +
              end
         | 
| 27 | 
            +
            end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
            def client_factory(config, keys, additions={})
         | 
| 30 | 
            +
              key = keys.detect { |key| config.has_key?(key) }
         | 
| 31 | 
            +
              return nil if key.nil?
         | 
| 32 | 
            +
              require_relative "../lib/integrations/#{key}_client"
         | 
| 33 | 
            +
              data = config[key].merge(additions)
         | 
| 34 | 
            +
              "#{key.camelize}Client".constantize.new(data)
         | 
| 35 | 
            +
            end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
            def scm_client(config)
         | 
| 38 | 
            +
              client_factory(config, ["git"])
         | 
| 39 | 
            +
            end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
            def issues(config, commits)
         | 
| 42 | 
            +
              client_factory(config, ["jira"])
         | 
| 43 | 
            +
                .try(:merge_and_fetch_issues!, commits) || {}
         | 
| 44 | 
            +
            end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
            def errors(config)
         | 
| 47 | 
            +
              client_factory(config, ["airbrake", "rollbar"])
         | 
| 48 | 
            +
            end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
            def coverage(config)
         | 
| 51 | 
            +
              client_factory(config, ["covhura"])
         | 
| 52 | 
            +
            end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
            def bugs(config, commits)
         | 
| 55 | 
            +
              client_factory(config, ["jira"])
         | 
| 56 | 
            +
                .try(:merge_and_fetch_bugs!, commits) || {}
         | 
| 57 | 
            +
            end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
             | 
| 60 | 
            +
            def index(branches)
         | 
| 61 | 
            +
              config = SecureFileStore.new(ENV["SETTING_KEY"]).read_settings()
         | 
| 62 | 
            +
              scm_client = scm_client(config)
         | 
| 63 | 
            +
             | 
| 64 | 
            +
              branches = branches.count == 0 ?
         | 
| 65 | 
            +
                scm_client.remote_branches() :
         | 
| 66 | 
            +
                branches
         | 
| 67 | 
            +
             | 
| 68 | 
            +
              branches.each do |branch|
         | 
| 69 | 
            +
                commits = scm_client.commits(branch)
         | 
| 70 | 
            +
                file_manager = FileManager.new(branch)
         | 
| 71 | 
            +
                file_manager.apply!(commits, {
         | 
| 72 | 
            +
                  issues: issues(config, commits),
         | 
| 73 | 
            +
                  errors: errors(config),
         | 
| 74 | 
            +
                  bugs: bugs(config, commits),
         | 
| 75 | 
            +
                  coverage: coverage(config)
         | 
| 76 | 
            +
                })
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                risk = RiskAnalyzer
         | 
| 79 | 
            +
                  .new
         | 
| 80 | 
            +
                  .analyze(file_manager, commits.last)
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                Notifier
         | 
| 83 | 
            +
                  .new
         | 
| 84 | 
            +
                  .notify(scm_client.notification_url, commits.last, risk)
         | 
| 85 | 
            +
              end
         | 
| 86 | 
            +
            end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
            def get_opts()
         | 
| 89 | 
            +
              options = {}
         | 
| 90 | 
            +
              OptionParser.new do |opts|
         | 
| 91 | 
            +
                opts.banner = "Usage: conglomerate.rb [options]"
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                opts.on("-p", "--repo-path=val", String, "Repository Path") do |path|
         | 
| 94 | 
            +
                  Dir.chdir(path)
         | 
| 95 | 
            +
                end
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                opts.on("-v", "--verbose", "Run Verbosely") do |verbose|
         | 
| 98 | 
            +
                  ENV["GITOLEMY_VERBOSE"] = "true"
         | 
| 99 | 
            +
                end
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                opts.on("-c", "--compare", "Compare virtual files to Git objects") do |compare|
         | 
| 102 | 
            +
                  ENV["GITOLEMY_COMPARE"] = "true"
         | 
| 103 | 
            +
                end
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                opts.on("-t", "--test", "Run as test: do not store results") do |test|
         | 
| 106 | 
            +
                  ENV["GITOLEMY_PERSIST"] = "false"
         | 
| 107 | 
            +
                end
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                opts.on("-s", "--sync", "Sync branch even if last commit indexed") do |sync|
         | 
| 110 | 
            +
                  ENV["GITOLEMY_SYNC"] = "true"
         | 
| 111 | 
            +
                end
         | 
| 112 | 
            +
              end.parse!
         | 
| 113 | 
            +
              options[:branches] = []
         | 
| 114 | 
            +
              options[:branches] << ARGV.first if ARGV.count > 0
         | 
| 115 | 
            +
              options
         | 
| 116 | 
            +
            end
         | 
| 117 | 
            +
             | 
| 118 | 
            +
             | 
| 119 | 
            +
            def main()
         | 
| 120 | 
            +
              index(get_opts()[:branches])
         | 
| 121 | 
            +
            rescue => ex
         | 
| 122 | 
            +
              #handler = proc do |options|
         | 
| 123 | 
            +
              #  payload = options[:payload]
         | 
| 124 | 
            +
              #  payload["data"]["environment"] = ENV["ROLLBAR_ENV"]
         | 
| 125 | 
            +
              #end
         | 
| 126 | 
            +
             | 
| 127 | 
            +
              #Rollbar.configure do |config|
         | 
| 128 | 
            +
              #  config.access_token = ENV["ROLLBAR_API_KEY"]
         | 
| 129 | 
            +
              #  config.transform << handler
         | 
| 130 | 
            +
              #end
         | 
| 131 | 
            +
             | 
| 132 | 
            +
              #Rollbar.error(ex) if ENV["ROLLBAR_ENV"] == "production"
         | 
| 133 | 
            +
             | 
| 134 | 
            +
              #Airbrake.configure do |c|
         | 
| 135 | 
            +
              #  c.project_id = ENV["AIRBRAKE_PROJECT_ID"]
         | 
| 136 | 
            +
              #  c.project_key = ENV["AIRBRAKE_PROJECT_KEY"]
         | 
| 137 | 
            +
              #  c.environment = ENV["AIRBRAKE_ENV"].to_sym
         | 
| 138 | 
            +
              #end
         | 
| 139 | 
            +
             | 
| 140 | 
            +
              #Airbrake.notify_sync(ex) if ENV["AIRBRAKE_ENV"] == "production"
         | 
| 141 | 
            +
             | 
| 142 | 
            +
              raise ex
         | 
| 143 | 
            +
            end
         | 
| 144 | 
            +
             | 
| 145 | 
            +
            main()
         | 
    
        data/bin/serve.rb
    ADDED
    
    | @@ -0,0 +1,59 @@ | |
| 1 | 
            +
            #! /usr/bin/env ruby
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "webrick"
         | 
| 4 | 
            +
            require "webrick/httpproxy"
         | 
| 5 | 
            +
            require "json"
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            server = WEBrick::HTTPProxyServer.new(:BindAddress => "0.0.0.0", :Port => 8180)
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            server.mount_proc "/sync" do |req, res|
         | 
| 10 | 
            +
              user = req.query["user"].gsub(" ", "+")
         | 
| 11 | 
            +
              repo = req.query["repo"]
         | 
| 12 | 
            +
              branch = req.query["branch"]
         | 
| 13 | 
            +
              is_sync = req.query["sync"] == "true"
         | 
| 14 | 
            +
             | 
| 15 | 
            +
              path = File.join(ENV.fetch("PROJECT_ROOT"), user, repo)
         | 
| 16 | 
            +
              if Dir.exist?(path)
         | 
| 17 | 
            +
                bin_path = File.join(File.dirname(__FILE__), "conglomerate.rb")
         | 
| 18 | 
            +
                pid = spawn("#{bin_path} #{'-s' if is_sync} #{branch} -p #{path}")
         | 
| 19 | 
            +
                Process.detach(pid)
         | 
| 20 | 
            +
                res.status = 200
         | 
| 21 | 
            +
              else
         | 
| 22 | 
            +
                res.status = 400
         | 
| 23 | 
            +
              end
         | 
| 24 | 
            +
            end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
            server.mount_proc "/progress" do |req, res|
         | 
| 27 | 
            +
              user = req.query["user"].gsub(" ", "+")
         | 
| 28 | 
            +
              repo = req.query["repo"]
         | 
| 29 | 
            +
              branch = req.query["branch"]
         | 
| 30 | 
            +
             | 
| 31 | 
            +
              path = File.join(ENV.fetch("PROJECT_ROOT"), user, repo)
         | 
| 32 | 
            +
              if Dir.exist?(path)
         | 
| 33 | 
            +
                resp = {
         | 
| 34 | 
            +
                  commits: commit_count(path),
         | 
| 35 | 
            +
                  indexed: indexed_count(path, branch)
         | 
| 36 | 
            +
                }
         | 
| 37 | 
            +
                res.status = 200
         | 
| 38 | 
            +
                res.body = resp.to_json
         | 
| 39 | 
            +
              else
         | 
| 40 | 
            +
                res.status = 400
         | 
| 41 | 
            +
              end
         | 
| 42 | 
            +
            end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
            def commit_count(project_root)
         | 
| 45 | 
            +
              git_dir = File.join(project_root, ".git")
         | 
| 46 | 
            +
              `git --git-dir=#{git_dir} log --graph --oneline | grep '^\*' | wc -l`.to_i
         | 
| 47 | 
            +
            end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
            def indexed_count(project_root, branch)
         | 
| 50 | 
            +
              branch = branch.gsub(/^remotes\/origin\//, "")
         | 
| 51 | 
            +
              branch_lock_path = File.join(project_root, ".gitolemy", "branches", "#{branch}.lock")
         | 
| 52 | 
            +
              `wc -l #{branch_lock_path}`
         | 
| 53 | 
            +
                .split(" ")
         | 
| 54 | 
            +
                .first
         | 
| 55 | 
            +
                .to_i
         | 
| 56 | 
            +
            end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
            trap("INT") { server.shutdown }
         | 
| 59 | 
            +
            server.start
         | 
    
        data/lib/cache.rb
    ADDED
    
    | @@ -0,0 +1,92 @@ | |
| 1 | 
            +
            require "json"
         | 
| 2 | 
            +
            require "zlib"
         | 
| 3 | 
            +
            require "fileutils"
         | 
| 4 | 
            +
            require "active_support/json"
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            module Cache
         | 
| 7 | 
            +
              extend self
         | 
| 8 | 
            +
             | 
| 9 | 
            +
              CACHE_BASE_PATH = ".gitolemy"
         | 
| 10 | 
            +
              OBJECT_CACHE_BASE = "objects"
         | 
| 11 | 
            +
              BRANCH_CACHE_BASE = "branches"
         | 
| 12 | 
            +
              REMOTES_REGEX = /^remotes\/origin\//
         | 
| 13 | 
            +
             | 
| 14 | 
            +
              def cache_path(key)
         | 
| 15 | 
            +
                File.join(CACHE_BASE_PATH, "#{key}.json.gz")
         | 
| 16 | 
            +
              end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
              def write(key, data, existing_data=nil)
         | 
| 19 | 
            +
                return data if not persist?
         | 
| 20 | 
            +
                filename = cache_path(key)
         | 
| 21 | 
            +
                dirname = File.dirname(filename)
         | 
| 22 | 
            +
                ensure_directory(dirname)
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                store_data = existing_data.nil? ? data : existing_data.merge(data)
         | 
| 25 | 
            +
                Zlib::GzipWriter.open(filename) { |gz| gz.write(store_data.to_json) }
         | 
| 26 | 
            +
                data
         | 
| 27 | 
            +
              end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
              def read(key, default_val=nil)
         | 
| 30 | 
            +
                filename = cache_path(key)
         | 
| 31 | 
            +
                return default_val if not File.exist?(filename)
         | 
| 32 | 
            +
                JSON.parse(Zlib::GzipReader.open(filename) { |gz| gz.read })
         | 
| 33 | 
            +
              rescue JSON::ParserError, Zlib::GzipFile::Error
         | 
| 34 | 
            +
                default_val
         | 
| 35 | 
            +
              end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
              def index_commit(branch, commit_id)
         | 
| 38 | 
            +
                return if not persist?
         | 
| 39 | 
            +
                branch_path = branch_path(branch)
         | 
| 40 | 
            +
                ensure_directory(File.dirname(branch_path))
         | 
| 41 | 
            +
                FileUtils.touch(branch_path) if not File.exist?(branch_path)
         | 
| 42 | 
            +
                File.open(branch_path, "a") do |file|
         | 
| 43 | 
            +
                  file << "#{commit_id.to_s}\n"
         | 
| 44 | 
            +
                end
         | 
| 45 | 
            +
              end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
              def index_commits(branch,  commits)
         | 
| 48 | 
            +
                return if not persist?
         | 
| 49 | 
            +
                File.write(branch_path(branch), commits.join("\n") + "\n")
         | 
| 50 | 
            +
              end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
              def last_indexed_commit(branch, commit_ids)
         | 
| 53 | 
            +
                cached_commit_ids = File
         | 
| 54 | 
            +
                  .read(branch_path(branch))
         | 
| 55 | 
            +
                  .lines
         | 
| 56 | 
            +
                  .map(&:chomp)
         | 
| 57 | 
            +
                  .reduce({}) do |acc, commit_id|
         | 
| 58 | 
            +
                    acc[commit_id.to_sym] = true
         | 
| 59 | 
            +
                    acc
         | 
| 60 | 
            +
                  end
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                commit_ids.detect { |commit_id| cached_commit_ids[commit_id] }
         | 
| 63 | 
            +
              rescue Errno::ENOENT
         | 
| 64 | 
            +
                nil
         | 
| 65 | 
            +
              end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
              def ensure_directory(dirname)
         | 
| 68 | 
            +
                return if File.directory?(dirname)
         | 
| 69 | 
            +
                FileUtils.makedirs(dirname)
         | 
| 70 | 
            +
              end
         | 
| 71 | 
            +
             | 
| 72 | 
            +
              def read_object(key, default_val=nil)
         | 
| 73 | 
            +
                read(object_rel_path(key.to_s), default_val)
         | 
| 74 | 
            +
              end
         | 
| 75 | 
            +
             | 
| 76 | 
            +
              def write_object(key, data)
         | 
| 77 | 
            +
                write(object_rel_path(key.to_s), data)
         | 
| 78 | 
            +
              end
         | 
| 79 | 
            +
             | 
| 80 | 
            +
              def object_rel_path(object_id)
         | 
| 81 | 
            +
                File.join(OBJECT_CACHE_BASE, object_id)
         | 
| 82 | 
            +
              end
         | 
| 83 | 
            +
             | 
| 84 | 
            +
              def branch_path(branch)
         | 
| 85 | 
            +
                branch_name = branch.gsub(REMOTES_REGEX, "")
         | 
| 86 | 
            +
                File.join(".gitolemy", BRANCH_CACHE_BASE, "#{branch_name}.lock")
         | 
| 87 | 
            +
              end
         | 
| 88 | 
            +
             | 
| 89 | 
            +
              def persist?
         | 
| 90 | 
            +
                ENV["GITOLEMY_PERSIST"] != "false"
         | 
| 91 | 
            +
              end
         | 
| 92 | 
            +
            end
         | 
    
        data/lib/commit.rb
    ADDED
    
    | @@ -0,0 +1,139 @@ | |
| 1 | 
            +
            require "mail"
         | 
| 2 | 
            +
            require "iconv"
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            require_relative "util"
         | 
| 5 | 
            +
            require_relative "loggr"
         | 
| 6 | 
            +
            require_relative "file_diff"
         | 
| 7 | 
            +
            require_relative "line_tracker"
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            class Commit
         | 
| 10 | 
            +
              FILE_DIFF_REGEX = /^diff --git a\/.* b\//
         | 
| 11 | 
            +
             | 
| 12 | 
            +
              attr_accessor :commit_id
         | 
| 13 | 
            +
              attr_accessor :children
         | 
| 14 | 
            +
              attr_accessor :author
         | 
| 15 | 
            +
              attr_accessor :date
         | 
| 16 | 
            +
              attr_accessor :subject
         | 
| 17 | 
            +
              attr_accessor :trees
         | 
| 18 | 
            +
              attr_accessor :file_diffs
         | 
| 19 | 
            +
              attr_accessor :movements
         | 
| 20 | 
            +
              attr_accessor :changes
         | 
| 21 | 
            +
              attr_accessor :insertions_total
         | 
| 22 | 
            +
              attr_accessor :deletions_total
         | 
| 23 | 
            +
              attr_accessor :changes_total
         | 
| 24 | 
            +
              attr_accessor :cached_files
         | 
| 25 | 
            +
              attr_accessor :cached_trees
         | 
| 26 | 
            +
              attr_accessor :issue_id
         | 
| 27 | 
            +
              attr_accessor :bug_id
         | 
| 28 | 
            +
             | 
| 29 | 
            +
              alias_method :id, :commit_id
         | 
| 30 | 
            +
              alias_method :id=, :commit_id=
         | 
| 31 | 
            +
             | 
| 32 | 
            +
              def initialize(attrs={})
         | 
| 33 | 
            +
                attrs.each do |key, val|
         | 
| 34 | 
            +
                  instance_variable_set("@#{key}".to_sym, val)
         | 
| 35 | 
            +
                end
         | 
| 36 | 
            +
              end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
              def set_cached(tree)
         | 
| 39 | 
            +
                @cached_files = tree.select { |obj| obj[:type] == :file }
         | 
| 40 | 
            +
                @cached_trees = tree.select { |obj| obj[:type] == :tree }
         | 
| 41 | 
            +
              end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
              def business_value
         | 
| 44 | 
            +
                issue = ::Store::Issue[issue_id]
         | 
| 45 | 
            +
                issue.present? ?
         | 
| 46 | 
            +
                  issue[:business_value] :
         | 
| 47 | 
            +
                  0
         | 
| 48 | 
            +
              end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
              def as_json
         | 
| 51 | 
            +
                {
         | 
| 52 | 
            +
                  commit_id: commit_id,
         | 
| 53 | 
            +
                  children: children,
         | 
| 54 | 
            +
                  author: author,
         | 
| 55 | 
            +
                  date: date,
         | 
| 56 | 
            +
                  subject: subject,
         | 
| 57 | 
            +
                  insertions_total: insertions_total,
         | 
| 58 | 
            +
                  deletions_total: deletions_total,
         | 
| 59 | 
            +
                  changes_total: changes.count,
         | 
| 60 | 
            +
                  movements_total: movements.count,
         | 
| 61 | 
            +
                  issue_id: issue_id,
         | 
| 62 | 
            +
                  bug_id: bug_id
         | 
| 63 | 
            +
                }
         | 
| 64 | 
            +
              end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
              class << self
         | 
| 67 | 
            +
                def from_git(commit_lines, git_client)
         | 
| 68 | 
            +
                  Commit.new(parse_commit(commit_lines, git_client))
         | 
| 69 | 
            +
                end
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                def parse_commit(commit_lines, git_client)
         | 
| 72 | 
            +
                  commit_id, author, date, children, subject = parse_commit_header(commit_lines.shift())
         | 
| 73 | 
            +
                  Loggr.instance.info("PARSE COMMIT: #{commit_id}")
         | 
| 74 | 
            +
                  if children.length > 1
         | 
| 75 | 
            +
                    commit_lines = git_client.diff(children.first, commit_id)
         | 
| 76 | 
            +
                  end
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                  file_diffs = commit_lines
         | 
| 79 | 
            +
                    .reduce([], &fold_reducer(FILE_DIFF_REGEX))
         | 
| 80 | 
            +
                    .map { |file_diff_lines| FileDiff.from_git(file_diff_lines) }
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                  insertions = file_diffs.reduce(0) { |sum, file_diff| sum + file_diff.insertions_total }
         | 
| 83 | 
            +
                  deletions = file_diffs.reduce(0) { |sum, file_diff| sum + file_diff.deletions_total }
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                  file_diffs = file_diffs.reduce({}) do |obj, file_diff|
         | 
| 86 | 
            +
                    obj[file_diff.b_file_name] = file_diff
         | 
| 87 | 
            +
                    obj
         | 
| 88 | 
            +
                  end
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                  if children.length > 1
         | 
| 91 | 
            +
                    diff_id = "#{children.first}..#{commit_id}"
         | 
| 92 | 
            +
                    trees = git_client.parse_diff_tree(git_client.diff_tree(diff_id))
         | 
| 93 | 
            +
                  elsif children.length == 1
         | 
| 94 | 
            +
                    trees = git_client.parse_diff_tree(git_client.diff_tree(commit_id))
         | 
| 95 | 
            +
                  else
         | 
| 96 | 
            +
                    trees = git_client.parse_ls_tree(git_client.ls_tree(commit_id))
         | 
| 97 | 
            +
                  end
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                  mutations = LineTracker.new.track_mutations!(file_diffs)
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                  {
         | 
| 102 | 
            +
                    commit_id: commit_id,
         | 
| 103 | 
            +
                    children: children,
         | 
| 104 | 
            +
                    author: {
         | 
| 105 | 
            +
                      name: author.display_name,
         | 
| 106 | 
            +
                      email: author.address
         | 
| 107 | 
            +
                    },
         | 
| 108 | 
            +
                    date: date,
         | 
| 109 | 
            +
                    subject: subject,
         | 
| 110 | 
            +
                    trees: trees,
         | 
| 111 | 
            +
                    file_diffs: file_diffs,
         | 
| 112 | 
            +
                    movements: mutations[:movements],
         | 
| 113 | 
            +
                    changes: mutations[:changes],
         | 
| 114 | 
            +
                    insertions_total: insertions,
         | 
| 115 | 
            +
                    deletions_total: deletions,
         | 
| 116 | 
            +
                    changes_total: insertions + deletions
         | 
| 117 | 
            +
                  }
         | 
| 118 | 
            +
                end
         | 
| 119 | 
            +
             | 
| 120 | 
            +
                def parse_commit_header(header)
         | 
| 121 | 
            +
                  commit_id, children, author, date, subject = header.split("|||")
         | 
| 122 | 
            +
                  commit_id = commit_id.to_sym
         | 
| 123 | 
            +
                  subject ||= ""
         | 
| 124 | 
            +
                  children = children
         | 
| 125 | 
            +
                    .split(" ")
         | 
| 126 | 
            +
                    .map { |child_id| child_id.to_sym }
         | 
| 127 | 
            +
                  author = parse_author(author)
         | 
| 128 | 
            +
                  date = DateTime.rfc2822(date)
         | 
| 129 | 
            +
                  [commit_id, author, date, children, subject]
         | 
| 130 | 
            +
                end
         | 
| 131 | 
            +
             | 
| 132 | 
            +
                def parse_author(author)
         | 
| 133 | 
            +
                  author = Iconv
         | 
| 134 | 
            +
                    .conv("ascii//translit", "UTF-8", author)
         | 
| 135 | 
            +
                    .tr("[]", "()")
         | 
| 136 | 
            +
                  Mail::Address.new(author)
         | 
| 137 | 
            +
                end
         | 
| 138 | 
            +
              end
         | 
| 139 | 
            +
            end
         | 
    
        data/lib/commit_stats.rb
    ADDED
    
    | @@ -0,0 +1,225 @@ | |
| 1 | 
            +
            require "diff_match_patch_native"
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module CommitStats
         | 
| 4 | 
            +
              def self.link_mutations!(files, commit_mutations, type)
         | 
| 5 | 
            +
                dmp = DiffMatchPatch.new()
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                commit_mutations.each do |dest_file, file_mutations|
         | 
| 8 | 
            +
                  file_mutations.each do |dest_line, mutation|
         | 
| 9 | 
            +
                    src_file = mutation[:from]
         | 
| 10 | 
            +
                    src_line = mutation[:line]
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                    b_file = files[:b][dest_file][:b_file]
         | 
| 13 | 
            +
                    a_file = files[:a][src_file][:a_file]
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                    b_line = b_file.is_a?(Hash) ? b_file[:lines][dest_line] : b_file.lines[dest_line]
         | 
| 16 | 
            +
                    a_line = a_file.is_a?(Hash) ? a_file[:lines][src_line] : a_file.lines[src_line]
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                    if type == :change
         | 
| 19 | 
            +
                      diff = dmp.diff_main(a_line[:text], b_line[:text], false)
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                      b_line[:diff_text] = diff.reduce("") do |acc, section|
         | 
| 22 | 
            +
                        if section.first == 0
         | 
| 23 | 
            +
                          acc += section.last
         | 
| 24 | 
            +
                        elsif section.first == 1
         | 
| 25 | 
            +
                          acc += "<ins>#{section.last}</ins>"
         | 
| 26 | 
            +
                        end
         | 
| 27 | 
            +
                        acc
         | 
| 28 | 
            +
                      end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                      a_line[:diff_text] = diff.reduce("") do |acc, section|
         | 
| 31 | 
            +
                        if section.first == 0
         | 
| 32 | 
            +
                          acc += section.last
         | 
| 33 | 
            +
                        elsif section.first == -1
         | 
| 34 | 
            +
                          acc += "<del>#{section.last}</del>"
         | 
| 35 | 
            +
                        end
         | 
| 36 | 
            +
                        acc
         | 
| 37 | 
            +
                      end
         | 
| 38 | 
            +
                    end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                    if Line.trailing?(b_line, a_line)
         | 
| 41 | 
            +
                      change_type = :trailing
         | 
| 42 | 
            +
                    elsif Line.beauty?(b_line, a_line)
         | 
| 43 | 
            +
                      change_type = :beauty
         | 
| 44 | 
            +
                    else
         | 
| 45 | 
            +
                      change_type = :normal
         | 
| 46 | 
            +
                    end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                    b_line[type] = {
         | 
| 49 | 
            +
                      link: "#/a/#{src_line + 1}/#{src_file}",
         | 
| 50 | 
            +
                      type: change_type,
         | 
| 51 | 
            +
                      change_text: a_line[:text]
         | 
| 52 | 
            +
                    }
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                    a_line[type] = {
         | 
| 55 | 
            +
                      link: "#/b/#{dest_line + 1}/#{dest_file}",
         | 
| 56 | 
            +
                      type: change_type,
         | 
| 57 | 
            +
                      change_text: b_line[:text]
         | 
| 58 | 
            +
                    }
         | 
| 59 | 
            +
                  end
         | 
| 60 | 
            +
                end
         | 
| 61 | 
            +
              end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
              def self.link_diffs!(files, file_diffs)
         | 
| 64 | 
            +
                file_diffs.reduce(diff_context()) do |acc, (file_name, file_diff)|
         | 
| 65 | 
            +
                  file_diff.diffs.each do |diff|
         | 
| 66 | 
            +
                    diff.insertions.each_with_index do |insertion, index|
         | 
| 67 | 
            +
                      line_num = diff.insert_start + index - 1
         | 
| 68 | 
            +
                      file = files[:b][file_diff.b_file_name][:b_file]
         | 
| 69 | 
            +
                      line = file.is_a?(Hash) ? file[:lines][line_num] : file.lines[line_num]
         | 
| 70 | 
            +
                      line[:insertion] = true
         | 
| 71 | 
            +
                      acc[:insertions] << line
         | 
| 72 | 
            +
                    end
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                    del_index = diff.insert_count > 0 ? 1 : 0
         | 
| 75 | 
            +
                    diff.deletions.each_with_index do |deletion, index|
         | 
| 76 | 
            +
                      line_num = diff.delete_start + index - 1
         | 
| 77 | 
            +
                      file = files[:a][file_diff.a_file_name][:a_file]
         | 
| 78 | 
            +
                      line = file.is_a?(Hash) ? file[:lines][line_num] : file.lines[line_num]
         | 
| 79 | 
            +
                      line.merge!({
         | 
| 80 | 
            +
                        deletion: true,
         | 
| 81 | 
            +
                        a_pos: line_num + 1,
         | 
| 82 | 
            +
                        b_pos: diff.insert_start + index - del_index
         | 
| 83 | 
            +
                      })
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                      del_index += 1 if line[:change].blank?
         | 
| 86 | 
            +
                      acc[:deletions] << line
         | 
| 87 | 
            +
                    end
         | 
| 88 | 
            +
                  end
         | 
| 89 | 
            +
                  acc
         | 
| 90 | 
            +
                end
         | 
| 91 | 
            +
              end
         | 
| 92 | 
            +
             | 
| 93 | 
            +
              def self.stats(commit, line_diffs, files)
         | 
| 94 | 
            +
                changes_total = commit
         | 
| 95 | 
            +
                  .changes
         | 
| 96 | 
            +
                  .reduce(0) { |acc, (filename, changeset)| acc += changeset.count } * 2
         | 
| 97 | 
            +
             | 
| 98 | 
            +
                movements_total = commit
         | 
| 99 | 
            +
                  .movements
         | 
| 100 | 
            +
                  .reduce(0) { |acc, (filename, moveset)| acc += moveset.count } * 2
         | 
| 101 | 
            +
             | 
| 102 | 
            +
                uncovered_deletions_total = 0
         | 
| 103 | 
            +
                uncovered_insertions_total = 0
         | 
| 104 | 
            +
                uncovered_changes_total = 0
         | 
| 105 | 
            +
                uncovered_error_changes_total = 0
         | 
| 106 | 
            +
                uncovered_buggy_changes_total = 0
         | 
| 107 | 
            +
                error_deletions_total = 0
         | 
| 108 | 
            +
                error_changes_total = 0
         | 
| 109 | 
            +
                buggy_insertions_total = 0
         | 
| 110 | 
            +
                buggy_deletions_total = 0
         | 
| 111 | 
            +
                buggy_changes_total = 0
         | 
| 112 | 
            +
                whitespace_insertions_total = 0
         | 
| 113 | 
            +
                whitespace_deletions_total = 0
         | 
| 114 | 
            +
                total_files_modified = files[:a].count
         | 
| 115 | 
            +
                total_lines_modified = commit.changes_total
         | 
| 116 | 
            +
                trailingspace_total = 0
         | 
| 117 | 
            +
                beautyspace_total = 0
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                line_diffs[:insertions].each do |insertion|
         | 
| 120 | 
            +
                  if insertion[:coverage] == 0
         | 
| 121 | 
            +
                    uncovered_insertions_total += 1
         | 
| 122 | 
            +
                    if insertion[:change]
         | 
| 123 | 
            +
                      uncovered_changes_total += 1
         | 
| 124 | 
            +
                      uncovered_error_changes_total += 1 if insertion[:errors].count > 0
         | 
| 125 | 
            +
                      uncovered_buggy_changes_total += 1 if insertion[:bugs].count > 0
         | 
| 126 | 
            +
                    end
         | 
| 127 | 
            +
                  end
         | 
| 128 | 
            +
                  if insertion[:change]
         | 
| 129 | 
            +
                    buggy_changes_total += 1 if insertion[:bugs].count > 0
         | 
| 130 | 
            +
                    error_changes_total += 1 if insertion[:errors].count > 0
         | 
| 131 | 
            +
                    beautyspace_total += 2 if insertion[:change][:type] == :beauty # + deletion
         | 
| 132 | 
            +
                    trailingspace_total += 2 if insertion[:change][:type] == :trailing # + deletion
         | 
| 133 | 
            +
                  end
         | 
| 134 | 
            +
                  whitespace_insertions_total += 1 if insertion[:text].strip().blank?
         | 
| 135 | 
            +
                  buggy_insertions_total += 1 if insertion[:bugs].count > 0
         | 
| 136 | 
            +
                end
         | 
| 137 | 
            +
             | 
| 138 | 
            +
                line_diffs[:deletions].each do |deletion|
         | 
| 139 | 
            +
                  if deletion[:coverage] == 0
         | 
| 140 | 
            +
                    uncovered_deletions_total += 1
         | 
| 141 | 
            +
                  end
         | 
| 142 | 
            +
                  error_deletions_total += 1 if deletion[:errors].count > 0
         | 
| 143 | 
            +
                  whitespace_deletions_total += 1 if deletion[:text].strip().blank?
         | 
| 144 | 
            +
                  buggy_deletions_total += 1 if deletion[:bugs].count > 0
         | 
| 145 | 
            +
                end
         | 
| 146 | 
            +
             | 
| 147 | 
            +
                relevant_insertions_total = commit.insertions_total - whitespace_insertions_total
         | 
| 148 | 
            +
                covered_insertions_total = relevant_insertions_total - uncovered_insertions_total
         | 
| 149 | 
            +
                covered_insertions_percent = change_percent(covered_insertions_total, relevant_insertions_total)
         | 
| 150 | 
            +
                uncovered_insertions_percent = change_percent(uncovered_insertions_total, relevant_insertions_total)
         | 
| 151 | 
            +
                buggy_insertions_percent = change_percent(buggy_insertions_total, relevant_insertions_total)
         | 
| 152 | 
            +
             | 
| 153 | 
            +
                relevant_deletions_total = commit.insertions_total - whitespace_insertions_total
         | 
| 154 | 
            +
                covered_deletions_total = relevant_deletions_total - uncovered_deletions_total
         | 
| 155 | 
            +
                covered_deletions_percent = change_percent(covered_deletions_total, relevant_deletions_total)
         | 
| 156 | 
            +
                uncovered_deletions_percent = change_percent(uncovered_deletions_total, relevant_deletions_total)
         | 
| 157 | 
            +
                buggy_deletions_percent = change_percent(buggy_deletions_total, relevant_deletions_total)
         | 
| 158 | 
            +
                error_deletions_percent = change_percent(error_deletions_total, relevant_deletions_total)
         | 
| 159 | 
            +
             | 
| 160 | 
            +
                covered_changes_total = changes_total - uncovered_changes_total
         | 
| 161 | 
            +
                covered_changes_percent = change_percent(covered_changes_total, changes_total)
         | 
| 162 | 
            +
                uncovered_changes_percent = change_percent(uncovered_changes_total, changes_total)
         | 
| 163 | 
            +
                uncovered_error_changes_percent = change_percent(uncovered_error_changes_total, changes_total)
         | 
| 164 | 
            +
                uncovered_buggy_changes_percent = change_percent(uncovered_buggy_changes_total, changes_total)
         | 
| 165 | 
            +
             | 
| 166 | 
            +
                diffs_total = commit.deletions_total + commit.insertions_total
         | 
| 167 | 
            +
                movements_percent = change_percent(movements_total, diffs_total)
         | 
| 168 | 
            +
             | 
| 169 | 
            +
                whitespace_total = whitespace_insertions_total + whitespace_deletions_total
         | 
| 170 | 
            +
                whitespace_percent = change_percent(whitespace_total, diffs_total)
         | 
| 171 | 
            +
                trailingspace_percent = change_percent(trailingspace_total, diffs_total)
         | 
| 172 | 
            +
                beautyspace_percent = change_percent(beautyspace_total, diffs_total)
         | 
| 173 | 
            +
             | 
| 174 | 
            +
                {
         | 
| 175 | 
            +
                  changes: commit.changes,
         | 
| 176 | 
            +
                  movements: commit.movements,
         | 
| 177 | 
            +
                  deletions_total: commit.deletions_total,
         | 
| 178 | 
            +
                  insertions_total: commit.insertions_total,
         | 
| 179 | 
            +
                  changes_total: changes_total,
         | 
| 180 | 
            +
                  movements_total: movements_total,
         | 
| 181 | 
            +
                  whitespace_total: whitespace_total,
         | 
| 182 | 
            +
                  trailingspace_total: trailingspace_total,
         | 
| 183 | 
            +
                  beautyspace_total: beautyspace_total,
         | 
| 184 | 
            +
                  covered_insertions_percent: covered_insertions_percent,
         | 
| 185 | 
            +
                  covered_deletions_percent: covered_deletions_percent,
         | 
| 186 | 
            +
                  covered_changes_percent: covered_changes_percent,
         | 
| 187 | 
            +
                  uncovered_insertions_percent: uncovered_insertions_percent,
         | 
| 188 | 
            +
                  uncovered_deletions_percent: uncovered_deletions_percent,
         | 
| 189 | 
            +
                  uncovered_changes_percent: uncovered_changes_percent,
         | 
| 190 | 
            +
                  uncovered_error_changes_percent: uncovered_error_changes_percent,
         | 
| 191 | 
            +
                  uncovered_buggy_changes_percent: uncovered_buggy_changes_percent,
         | 
| 192 | 
            +
                  buggy_insertions_percent: buggy_insertions_percent,
         | 
| 193 | 
            +
                  buggy_deletions_percent: buggy_deletions_percent,
         | 
| 194 | 
            +
                  error_deletions_percent: error_deletions_percent,
         | 
| 195 | 
            +
                  movements_percent: movements_percent,
         | 
| 196 | 
            +
                  whitespace_percent: whitespace_percent,
         | 
| 197 | 
            +
                  trailingspace_percent: trailingspace_percent,
         | 
| 198 | 
            +
                  beautyspace_percent: beautyspace_percent,
         | 
| 199 | 
            +
                  uncovered_deletions_total: uncovered_deletions_total,
         | 
| 200 | 
            +
                  uncovered_insertions_total: uncovered_insertions_total,
         | 
| 201 | 
            +
                  uncovered_changes_total: uncovered_changes_total,
         | 
| 202 | 
            +
                  uncovered_error_changes_total: uncovered_error_changes_total,
         | 
| 203 | 
            +
                  buggy_insertions_total: buggy_insertions_total,
         | 
| 204 | 
            +
                  buggy_deletions_total: buggy_deletions_total,
         | 
| 205 | 
            +
                  buggy_changes_total: buggy_changes_total,
         | 
| 206 | 
            +
                  uncovered_buggy_changes_total: uncovered_buggy_changes_total,
         | 
| 207 | 
            +
                  error_deletions_total: error_deletions_total,
         | 
| 208 | 
            +
                  error_changes_total: error_changes_total,
         | 
| 209 | 
            +
                  total_files_modified: total_files_modified,
         | 
| 210 | 
            +
                  total_lines_modified: total_lines_modified
         | 
| 211 | 
            +
                }
         | 
| 212 | 
            +
              end
         | 
| 213 | 
            +
             | 
| 214 | 
            +
              def self.change_percent(numerator, denominator)
         | 
| 215 | 
            +
                denominator > 0 ?
         | 
| 216 | 
            +
                  (numerator / denominator.to_f * 100) :
         | 
| 217 | 
            +
                  0
         | 
| 218 | 
            +
              end
         | 
| 219 | 
            +
             | 
| 220 | 
            +
              def self.diff_context()
         | 
| 221 | 
            +
                {
         | 
| 222 | 
            +
                  insertions: [],
         | 
| 223 | 
            +
                  deletions: [] }
         | 
| 224 | 
            +
              end
         | 
| 225 | 
            +
            end
         |