├── spec ├── commit_spec.rb ├── dummy │ ├── hi │ ├── dot_git │ │ ├── HEAD │ │ ├── refs │ │ │ ├── heads │ │ │ │ └── master │ │ │ └── remotes │ │ │ │ └── origin │ │ │ │ └── HEAD │ │ ├── description │ │ ├── index │ │ ├── FETCH_HEAD │ │ ├── packed-refs │ │ ├── objects │ │ │ ├── 22 │ │ │ │ └── 54dd56976b5f32a2289438842e42a35a18ff86 │ │ │ ├── 25 │ │ │ │ └── 1af04ac94bf6e07db7a96355c1c92abcaab15e │ │ │ ├── 57 │ │ │ │ ├── 0f1df0505ed828656eeaf9411ddd6a6068b095 │ │ │ │ └── b0d5cdb6da2b2b9ac7e9847716b0c54466d1c6 │ │ │ ├── 65 │ │ │ │ ├── 74f08d639b3bd239dc725d5ca6967277c7c745 │ │ │ │ └── f07148391ae6ae503b386cc23d305d77bc0fa1 │ │ │ ├── 88 │ │ │ │ └── 6876e0158aa3a5ea31f30bfd1e88af8196ea4d │ │ │ ├── 91 │ │ │ │ └── 1ac42c4f68e887f6228ea7e2f275fd574fda7c │ │ │ ├── 99 │ │ │ │ └── b14a6222becff7970dc0534a075c0902479548 │ │ │ ├── 05 │ │ │ │ └── 2ff8c0e8c7cd39880d1536f4e27cc554e698f6 │ │ │ ├── 2f │ │ │ │ └── 43ad7b44490c466283dd479486295ffbd512c0 │ │ │ ├── 4c │ │ │ │ └── 1ea35f9a811a0ef79da15ec85f25fce4c446ba │ │ │ ├── 8b │ │ │ │ └── d6648ed130ac9ece0f89cd9a8fbbfd2608427a │ │ │ ├── a4 │ │ │ │ └── c26aeb79acb1f012201fe96e4d68e8d17c75d9 │ │ │ ├── c2 │ │ │ │ └── 841a877d895366cdc32232c1f63828764e05d3 │ │ │ ├── d3 │ │ │ │ └── c45a1a66631f5e6e54283473210a0b20a4c239 │ │ │ ├── d6 │ │ │ │ ├── 42b3c04c3025655a9c33e32b9d530696dcf7cc │ │ │ │ └── 7eafabb71ca9f0d847134e9a0e864f37013cf6 │ │ │ ├── dc │ │ │ │ └── d748bd06b8a0f239d779bee4f1eaf1f4aa500d │ │ │ ├── e4 │ │ │ │ └── 93d6f2ab2a702fa7f9c168b852a3b44c524f08 │ │ │ ├── e5 │ │ │ │ └── 11abe8371d018086bfd23b73e8fe483194f5c7 │ │ │ ├── e6 │ │ │ │ └── 9de29bb2d1d6434b8b29ae775ad8c2e48c5391 │ │ │ ├── e8 │ │ │ │ └── 0ad49ace82167de62e498622d70377d913c79e │ │ │ └── ff │ │ │ │ └── 6b04c348b3aa1bf8d2e4652f86ebd63a371aed │ │ ├── sourcetreeconfig │ │ ├── logs │ │ │ ├── HEAD │ │ │ └── refs │ │ │ │ ├── heads │ │ │ │ └── master │ │ │ │ └── remotes │ │ │ │ └── origin │ │ │ │ └── HEAD │ │ ├── hooks │ │ │ ├── post-update.sample │ │ │ ├── pre-applypatch.sample │ │ │ ├── applypatch-msg.sample │ │ │ ├── commit-msg.sample │ │ │ ├── prepare-commit-msg.sample │ │ │ ├── pre-push.sample │ │ │ ├── pre-commit.sample │ │ │ ├── update.sample │ │ │ └── pre-rebase.sample │ │ ├── info │ │ │ └── exclude │ │ └── config │ └── readme ├── fixtures │ └── wakatime.yml ├── durations_spec.rb ├── request_builder_spec.rb ├── cli_spec.rb ├── mapper_spec.rb ├── query_spec.rb ├── spec_helper.rb ├── timer_spec.rb └── commited_file_spec.rb ├── .rspec ├── lib ├── gitwakatime │ ├── version.rb │ ├── models │ │ ├── heartbeat.rb │ │ ├── commit.rb │ │ └── commited_file.rb │ ├── log.rb │ ├── request.rb │ ├── request_builder.rb │ ├── mapper.rb │ ├── controller.rb │ ├── durations_calculator.rb │ ├── query.rb │ ├── timer.rb │ └── cli.rb └── gitwakatime.rb ├── bin └── gitwakatime ├── Gemfile ├── .gitignore ├── .codeclimate.yml ├── Rakefile ├── .travis.yml ├── LICENSE.txt ├── gitwakatime.gemspec └── README.md /spec/commit_spec.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/hi: -------------------------------------------------------------------------------- 1 | asdf 2 | fdsasdq 3 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format progress 3 | -------------------------------------------------------------------------------- /spec/dummy/dot_git/HEAD: -------------------------------------------------------------------------------- 1 | ref: refs/heads/master 2 | -------------------------------------------------------------------------------- /spec/dummy/dot_git/refs/heads/master: -------------------------------------------------------------------------------- 1 | a4c26aeb79acb1f012201fe96e4d68e8d17c75d9 2 | -------------------------------------------------------------------------------- /spec/dummy/dot_git/refs/remotes/origin/HEAD: -------------------------------------------------------------------------------- 1 | ref: refs/remotes/origin/master 2 | -------------------------------------------------------------------------------- /spec/fixtures/wakatime.yml: -------------------------------------------------------------------------------- 1 | :api_key: 1234-1234-1234-1234 2 | :last_commit: 3 | :log_level: :info 4 | -------------------------------------------------------------------------------- /spec/dummy/dot_git/description: -------------------------------------------------------------------------------- 1 | Unnamed repository; edit this file 'description' to name the repository. 2 | -------------------------------------------------------------------------------- /spec/dummy/dot_git/index: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rposborne/gitwakatime/HEAD/spec/dummy/dot_git/index -------------------------------------------------------------------------------- /lib/gitwakatime/version.rb: -------------------------------------------------------------------------------- 1 | # Version Number Definition 2 | module GitWakaTime 3 | VERSION = '0.4.0' 4 | end 5 | -------------------------------------------------------------------------------- /spec/durations_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe GitWakaTime::DurationsCalculator do 4 | end 5 | -------------------------------------------------------------------------------- /lib/gitwakatime/models/heartbeat.rb: -------------------------------------------------------------------------------- 1 | module GitWakaTime 2 | class Heartbeat < Sequel::Model 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /spec/dummy/dot_git/FETCH_HEAD: -------------------------------------------------------------------------------- 1 | a4c26aeb79acb1f012201fe96e4d68e8d17c75d9 branch 'master' of https://github.com/rposborne/dummy_repo 2 | -------------------------------------------------------------------------------- /bin/gitwakatime: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pry' 3 | require 'thor' 4 | require_relative '../lib/gitwakatime' 5 | GitWakaTime::Cli.start 6 | -------------------------------------------------------------------------------- /spec/dummy/dot_git/packed-refs: -------------------------------------------------------------------------------- 1 | # pack-refs with: peeled fully-peeled 2 | a4c26aeb79acb1f012201fe96e4d68e8d17c75d9 refs/remotes/origin/master 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gem 'simplecov', group: :test, require: nil 3 | # Specify your gem's dependencies in gitwakatime.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /spec/dummy/readme: -------------------------------------------------------------------------------- 1 | ## Move along, this repo is for testing purposes only. 2 | 3 | I was edited online using git hub's editor, therefore should accru no time for this commit. 4 | -------------------------------------------------------------------------------- /spec/dummy/dot_git/objects/05/2ff8c0e8c7cd39880d1536f4e27cc554e698f6: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rposborne/gitwakatime/HEAD/spec/dummy/dot_git/objects/05/2ff8c0e8c7cd39880d1536f4e27cc554e698f6 -------------------------------------------------------------------------------- /spec/dummy/dot_git/objects/22/54dd56976b5f32a2289438842e42a35a18ff86: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rposborne/gitwakatime/HEAD/spec/dummy/dot_git/objects/22/54dd56976b5f32a2289438842e42a35a18ff86 -------------------------------------------------------------------------------- /spec/dummy/dot_git/objects/25/1af04ac94bf6e07db7a96355c1c92abcaab15e: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rposborne/gitwakatime/HEAD/spec/dummy/dot_git/objects/25/1af04ac94bf6e07db7a96355c1c92abcaab15e -------------------------------------------------------------------------------- /spec/dummy/dot_git/objects/2f/43ad7b44490c466283dd479486295ffbd512c0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rposborne/gitwakatime/HEAD/spec/dummy/dot_git/objects/2f/43ad7b44490c466283dd479486295ffbd512c0 -------------------------------------------------------------------------------- /spec/dummy/dot_git/objects/4c/1ea35f9a811a0ef79da15ec85f25fce4c446ba: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rposborne/gitwakatime/HEAD/spec/dummy/dot_git/objects/4c/1ea35f9a811a0ef79da15ec85f25fce4c446ba -------------------------------------------------------------------------------- /spec/dummy/dot_git/objects/57/0f1df0505ed828656eeaf9411ddd6a6068b095: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rposborne/gitwakatime/HEAD/spec/dummy/dot_git/objects/57/0f1df0505ed828656eeaf9411ddd6a6068b095 -------------------------------------------------------------------------------- /spec/dummy/dot_git/objects/57/b0d5cdb6da2b2b9ac7e9847716b0c54466d1c6: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rposborne/gitwakatime/HEAD/spec/dummy/dot_git/objects/57/b0d5cdb6da2b2b9ac7e9847716b0c54466d1c6 -------------------------------------------------------------------------------- /spec/dummy/dot_git/objects/65/74f08d639b3bd239dc725d5ca6967277c7c745: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rposborne/gitwakatime/HEAD/spec/dummy/dot_git/objects/65/74f08d639b3bd239dc725d5ca6967277c7c745 -------------------------------------------------------------------------------- /spec/dummy/dot_git/objects/65/f07148391ae6ae503b386cc23d305d77bc0fa1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rposborne/gitwakatime/HEAD/spec/dummy/dot_git/objects/65/f07148391ae6ae503b386cc23d305d77bc0fa1 -------------------------------------------------------------------------------- /spec/dummy/dot_git/objects/88/6876e0158aa3a5ea31f30bfd1e88af8196ea4d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rposborne/gitwakatime/HEAD/spec/dummy/dot_git/objects/88/6876e0158aa3a5ea31f30bfd1e88af8196ea4d -------------------------------------------------------------------------------- /spec/dummy/dot_git/objects/8b/d6648ed130ac9ece0f89cd9a8fbbfd2608427a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rposborne/gitwakatime/HEAD/spec/dummy/dot_git/objects/8b/d6648ed130ac9ece0f89cd9a8fbbfd2608427a -------------------------------------------------------------------------------- /spec/dummy/dot_git/objects/91/1ac42c4f68e887f6228ea7e2f275fd574fda7c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rposborne/gitwakatime/HEAD/spec/dummy/dot_git/objects/91/1ac42c4f68e887f6228ea7e2f275fd574fda7c -------------------------------------------------------------------------------- /spec/dummy/dot_git/objects/99/b14a6222becff7970dc0534a075c0902479548: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rposborne/gitwakatime/HEAD/spec/dummy/dot_git/objects/99/b14a6222becff7970dc0534a075c0902479548 -------------------------------------------------------------------------------- /spec/dummy/dot_git/objects/a4/c26aeb79acb1f012201fe96e4d68e8d17c75d9: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rposborne/gitwakatime/HEAD/spec/dummy/dot_git/objects/a4/c26aeb79acb1f012201fe96e4d68e8d17c75d9 -------------------------------------------------------------------------------- /spec/dummy/dot_git/objects/c2/841a877d895366cdc32232c1f63828764e05d3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rposborne/gitwakatime/HEAD/spec/dummy/dot_git/objects/c2/841a877d895366cdc32232c1f63828764e05d3 -------------------------------------------------------------------------------- /spec/dummy/dot_git/objects/d3/c45a1a66631f5e6e54283473210a0b20a4c239: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rposborne/gitwakatime/HEAD/spec/dummy/dot_git/objects/d3/c45a1a66631f5e6e54283473210a0b20a4c239 -------------------------------------------------------------------------------- /spec/dummy/dot_git/objects/d6/42b3c04c3025655a9c33e32b9d530696dcf7cc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rposborne/gitwakatime/HEAD/spec/dummy/dot_git/objects/d6/42b3c04c3025655a9c33e32b9d530696dcf7cc -------------------------------------------------------------------------------- /spec/dummy/dot_git/objects/d6/7eafabb71ca9f0d847134e9a0e864f37013cf6: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rposborne/gitwakatime/HEAD/spec/dummy/dot_git/objects/d6/7eafabb71ca9f0d847134e9a0e864f37013cf6 -------------------------------------------------------------------------------- /spec/dummy/dot_git/objects/dc/d748bd06b8a0f239d779bee4f1eaf1f4aa500d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rposborne/gitwakatime/HEAD/spec/dummy/dot_git/objects/dc/d748bd06b8a0f239d779bee4f1eaf1f4aa500d -------------------------------------------------------------------------------- /spec/dummy/dot_git/objects/e4/93d6f2ab2a702fa7f9c168b852a3b44c524f08: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rposborne/gitwakatime/HEAD/spec/dummy/dot_git/objects/e4/93d6f2ab2a702fa7f9c168b852a3b44c524f08 -------------------------------------------------------------------------------- /spec/dummy/dot_git/objects/e5/11abe8371d018086bfd23b73e8fe483194f5c7: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rposborne/gitwakatime/HEAD/spec/dummy/dot_git/objects/e5/11abe8371d018086bfd23b73e8fe483194f5c7 -------------------------------------------------------------------------------- /spec/dummy/dot_git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rposborne/gitwakatime/HEAD/spec/dummy/dot_git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 -------------------------------------------------------------------------------- /spec/dummy/dot_git/objects/e8/0ad49ace82167de62e498622d70377d913c79e: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rposborne/gitwakatime/HEAD/spec/dummy/dot_git/objects/e8/0ad49ace82167de62e498622d70377d913c79e -------------------------------------------------------------------------------- /spec/dummy/dot_git/objects/ff/6b04c348b3aa1bf8d2e4652f86ebd63a371aed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rposborne/gitwakatime/HEAD/spec/dummy/dot_git/objects/ff/6b04c348b3aa1bf8d2e4652f86ebd63a371aed -------------------------------------------------------------------------------- /spec/dummy/dot_git/sourcetreeconfig: -------------------------------------------------------------------------------- 1 | autorefresh=1 2 | savedIncoming=0 3 | lastUsedView=0 4 | savedOutgoing=0 5 | lastCheckedRemotes=2015-02-03 06:54:37 +0000 6 | disablerecursiveoperations=0 7 | autorefreshremotes=1 8 | -------------------------------------------------------------------------------- /spec/dummy/dot_git/logs/HEAD: -------------------------------------------------------------------------------- 1 | 0000000000000000000000000000000000000000 a4c26aeb79acb1f012201fe96e4d68e8d17c75d9 Russell Osborne 1422737446 -0500 clone: from https://github.com/rposborne/dummy_repo.git 2 | -------------------------------------------------------------------------------- /spec/dummy/dot_git/logs/refs/heads/master: -------------------------------------------------------------------------------- 1 | 0000000000000000000000000000000000000000 a4c26aeb79acb1f012201fe96e4d68e8d17c75d9 Russell Osborne 1422737446 -0500 clone: from https://github.com/rposborne/dummy_repo.git 2 | -------------------------------------------------------------------------------- /spec/dummy/dot_git/logs/refs/remotes/origin/HEAD: -------------------------------------------------------------------------------- 1 | 0000000000000000000000000000000000000000 a4c26aeb79acb1f012201fe96e4d68e8d17c75d9 Russell Osborne 1422737446 -0500 clone: from https://github.com/rposborne/dummy_repo.git 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | bin/rspec 19 | -------------------------------------------------------------------------------- /spec/dummy/dot_git/hooks/post-update.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to prepare a packed repository for use over 4 | # dumb transports. 5 | # 6 | # To enable this hook, rename this file to "post-update". 7 | 8 | exec git update-server-info 9 | -------------------------------------------------------------------------------- /spec/dummy/dot_git/info/exclude: -------------------------------------------------------------------------------- 1 | # git ls-files --others --exclude-from=.git/info/exclude 2 | # Lines that start with '#' are comments. 3 | # For a project mostly in C, the following would be a good set of 4 | # exclude patterns (uncomment them if you want to use them): 5 | # *.[oa] 6 | # *~ 7 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | engines: 3 | fixme: 4 | enabled: true 5 | rubocop: 6 | enabled: true 7 | brakeman: 8 | enabled: true 9 | bundler-audit: 10 | enabled: true 11 | duplication: 12 | enabled: true 13 | config: 14 | languages: 15 | - ruby 16 | ratings: 17 | paths: 18 | - "**.rb" 19 | exclude_paths: 20 | - spec/**/* 21 | - ".codeclimate.yml" 22 | - ".rubocop.yml" 23 | -------------------------------------------------------------------------------- /spec/dummy/dot_git/config: -------------------------------------------------------------------------------- 1 | [core] 2 | repositoryformatversion = 0 3 | filemode = true 4 | bare = false 5 | logallrefupdates = true 6 | worktree = ../../../../spec/dummy 7 | ignorecase = true 8 | precomposeunicode = true 9 | [remote "origin"] 10 | url = https://github.com/rposborne/dummy_repo.git 11 | fetch = +refs/heads/*:refs/remotes/origin/* 12 | [branch "master"] 13 | remote = origin 14 | merge = refs/heads/master 15 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | begin 3 | Bundler.setup(:default, :development) 4 | rescue Bundler::BundlerError => e 5 | $stderr.puts e.message 6 | $stderr.puts 'Run `bundle install` to install missing gems' 7 | exit e.status_code 8 | end 9 | require 'rake' 10 | 11 | begin 12 | require 'rspec/core/rake_task' 13 | 14 | RSpec::Core::RakeTask.new(:spec) 15 | 16 | task default: :spec 17 | rescue LoadError 18 | # no rspec available 19 | end 20 | -------------------------------------------------------------------------------- /lib/gitwakatime/log.rb: -------------------------------------------------------------------------------- 1 | module GitWakaTime 2 | ## 3 | # Pretty output, and ability to silence in testing 4 | class Log 5 | def initialize(msg, color = nil) 6 | @color = color 7 | @msg = msg 8 | print_message 9 | end 10 | 11 | def print_message 12 | return if ENV['waka_log'] == 'false' 13 | if @color.nil? 14 | puts @msg 15 | else 16 | puts @msg.send(@color) 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/dummy/dot_git/hooks/pre-applypatch.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to verify what is about to be committed 4 | # by applypatch from an e-mail message. 5 | # 6 | # The hook should exit with non-zero status after issuing an 7 | # appropriate message if it wants to stop the commit. 8 | # 9 | # To enable this hook, rename this file to "pre-applypatch". 10 | 11 | . git-sh-setup 12 | test -x "$GIT_DIR/hooks/pre-commit" && 13 | exec "$GIT_DIR/hooks/pre-commit" ${1+"$@"} 14 | : 15 | -------------------------------------------------------------------------------- /spec/dummy/dot_git/hooks/applypatch-msg.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to check the commit log message taken by 4 | # applypatch from an e-mail message. 5 | # 6 | # The hook should exit with non-zero status after issuing an 7 | # appropriate message if it wants to stop the commit. The hook is 8 | # allowed to edit the commit message file. 9 | # 10 | # To enable this hook, rename this file to "applypatch-msg". 11 | 12 | . git-sh-setup 13 | test -x "$GIT_DIR/hooks/commit-msg" && 14 | exec "$GIT_DIR/hooks/commit-msg" ${1+"$@"} 15 | : 16 | -------------------------------------------------------------------------------- /spec/request_builder_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe GitWakaTime::RequestBuilder do 4 | subject(:subject) { described_class.new(3.days.ago, Time.now).call } 5 | 6 | it 'returns a array of hashes' do 7 | expect(subject).to be_a(Array) 8 | expect(subject.first).to be_a(Hash) 9 | end 10 | 11 | it 'has a hash per day' do 12 | expect(subject.size).to eq(4) 13 | end 14 | 15 | it 'has a first hash will be for 3 days ago' do 16 | expect(subject.first).to eq( 17 | date: 3.days.ago.to_date, 18 | show: 'file,branch,project,time,id' 19 | ) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | sudo: false 3 | rvm: 4 | - 2.5.0 5 | 6 | before_script: 7 | - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 8 | - chmod +x ./cc-test-reporter 9 | - ./cc-test-reporter before-build 10 | 11 | script: bundle exec rake spec 12 | 13 | addons: 14 | code_climate: 15 | repo_token: 214446b5fc4f8697cc9ccc3ab1f2612c2e083ea1e71266648319663719cf85b9 16 | 17 | env: 18 | global: 19 | - CC_TEST_REPORTER_ID=214446b5fc4f8697cc9ccc3ab1f2612c2e083ea1e71266648319663719cf85b9 20 | 21 | after_script: 22 | - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT 23 | -------------------------------------------------------------------------------- /lib/gitwakatime/request.rb: -------------------------------------------------------------------------------- 1 | require 'benchmark' 2 | require 'colorize' 3 | 4 | module GitWakaTime 5 | # Integrates the nested hash from mapper with heartbeats api 6 | class Request 7 | def initialize(args) 8 | @args = args 9 | @heartbeats = [] 10 | @session = Wakatime::Session.new(api_key: GitWakaTime.config.api_key) 11 | @client = Wakatime::Client.new(@session) 12 | end 13 | 14 | def call 15 | Log.new "Gettting heartbeats #{@args[:date]}".red 16 | time = Benchmark.realtime do 17 | @result = @client.heartbeats(@args) || [] 18 | end 19 | Log.new "API took #{time}s" 20 | 21 | @result 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/cli_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe GitWakaTime::Cli do 4 | before do 5 | ENV['waka_log'] = 'true' 6 | stub_request(:get, /wakatime\.com/) 7 | .with(query: hash_including(:date)) 8 | .to_return(body: File.read('./spec/fixtures/heartbeats.json'), status: 200) 9 | 10 | expect(YAML).to receive(:load_file).with(File.expand_path('~/.wakatime.yml')).and_return(YAML.load_file('./spec/fixtures/wakatime.yml')) 11 | end 12 | 13 | after do 14 | ENV['waka_log'] = 'false' 15 | end 16 | 17 | it 'should be able to be called' do 18 | ARGV.replace %w(tally --start_on 2012-01-01 --file) << @wdir.to_s 19 | expect { GitWakaTime::Cli.start }.to output.to_stdout 20 | # puts GitWakaTime::Cli.start('tally', "--file #{@wdir.to_s}") 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/dummy/dot_git/hooks/commit-msg.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to check the commit log message. 4 | # Called by "git commit" with one argument, the name of the file 5 | # that has the commit message. The hook should exit with non-zero 6 | # status after issuing an appropriate message if it wants to stop the 7 | # commit. The hook is allowed to edit the commit message file. 8 | # 9 | # To enable this hook, rename this file to "commit-msg". 10 | 11 | # Uncomment the below to add a Signed-off-by line to the message. 12 | # Doing this in a hook is a bad idea in general, but the prepare-commit-msg 13 | # hook is more suited to it. 14 | # 15 | # SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') 16 | # grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" 17 | 18 | # This example catches duplicate Signed-off-by lines. 19 | 20 | test "" = "$(grep '^Signed-off-by: ' "$1" | 21 | sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { 22 | echo >&2 Duplicate Signed-off-by lines. 23 | exit 1 24 | } 25 | -------------------------------------------------------------------------------- /lib/gitwakatime/request_builder.rb: -------------------------------------------------------------------------------- 1 | module GitWakaTime 2 | # Build an array of hash's (params) that can be iterated over for the 3 | # wakatime API. 4 | class RequestBuilder 5 | WAKATIME_EPOCH = Date.new(2013, 5, 1) 6 | API_LIMIT = 1 # API ONLY ACCEPTS 1 day 7 | 8 | def initialize(start_at, end_at) 9 | @start_at = [start_at.to_date, WAKATIME_EPOCH].max 10 | @end_at = end_at.to_date 11 | end 12 | 13 | def call 14 | # Always have a date range great than 1 as the num request 15 | # will be 0/1 otherwise 16 | num_requests = ((@end_at + 1) - @start_at) / API_LIMIT 17 | i = 0 18 | 19 | request_params = num_requests.to_f.ceil.times.map do 20 | params = construct_params(i) 21 | i += 1 22 | params 23 | end 24 | request_params 25 | end 26 | 27 | private 28 | 29 | def construct_params(i) 30 | { 31 | date: (@start_at.to_date + i).to_date, 32 | show: 'file,branch,project,time,id' 33 | } 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/mapper_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'description' do 4 | let(:git) { Git.open(@wdir) } 5 | before(:each) do 6 | GitWakaTime.config.git = git 7 | GitWakaTime::Mapper.new(start_at: Date.new(2015, 1, 25)) 8 | end 9 | 10 | it 'can be run on dummy' do 11 | expect( 12 | GitWakaTime::Commit.all.size 13 | ).to eq 7 # 9ths is lonely 14 | end 15 | it 'can be run on dummy' do 16 | expect( 17 | GitWakaTime::Commit.order(:date).first.message 18 | ).to eq 'created readme' 19 | end 20 | 21 | it 'maps files dependent commits' do 22 | expect( 23 | GitWakaTime::Commit.all.first.commited_files.first.dependent_sha 24 | ).to eq 'dcd748bd06b8a0f239d779bee4f1eaf1f4aa500d' 25 | end 26 | 27 | it 'maps files dependent commits' do 28 | expect( 29 | GitWakaTime::Commit.all.find do |c| 30 | c.sha == 'dcd748bd06b8a0f239d779bee4f1eaf1f4aa500d' 31 | end.commited_files.first.dependent_sha 32 | ).to eq '2254dd56976b5f32a2289438842e42a35a18ff86' 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/gitwakatime/mapper.rb: -------------------------------------------------------------------------------- 1 | module GitWakaTime 2 | # Th 3 | class Mapper 4 | attr_accessor :commits, :git 5 | def initialize(commits: 500, start_at: Date.today) 6 | Log.new 'Mapping commits for dependent commits' 7 | time = Benchmark.realtime do 8 | g = GitWakaTime.config.git 9 | project = File.basename(g.dir.path) 10 | logs = g.log(commits).since(start_at).until(Date.today) 11 | 12 | @commits = logs.map do |git_c| 13 | next if git_c.author.name != GitWakaTime.config.user_name 14 | next if git_c.parents.size > 1 15 | 16 | Commit.find_or_create( 17 | sha: git_c.sha, 18 | project: project 19 | ) do |c| 20 | c.update( 21 | author: git_c.author.name, 22 | message: git_c.message, 23 | date: git_c.date.utc 24 | ) 25 | end 26 | end.compact 27 | end 28 | Log.new "Map Completed took #{time}s with #{@commits.size} commits" 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/gitwakatime/controller.rb: -------------------------------------------------------------------------------- 1 | module GitWakaTime 2 | # Extract Duration Data from Heartbeats for the WAKATIME API 3 | class Controller 4 | attr_accessor :time_range, :heartbeats, :relevant_commits, :project 5 | 6 | def initialize(path: '.', date: nil) 7 | @path = path 8 | GitWakaTime.config.setup_local_db 9 | GitWakaTime.config.root = path 10 | GitWakaTime.config.load_config_yaml 11 | GitWakaTime.config.git = Git.open(path) 12 | GitWakaTime::Query.new(date, Date.today, @project).call 13 | 14 | @git_map = Mapper.new(start_at: date) 15 | @project = File.basename(GitWakaTime.config.git.dir.path) 16 | @relevant_commits = Commit.where( project: @project ) 17 | 18 | # Scope by date if one has been passed 19 | @relevant_commits = @relevant_commits.where(Sequel[:date] > date) if date 20 | 21 | @files = CommitedFile.where( 22 | commit_id: @relevant_commits.select_map(:id) 23 | ).where(project: @project) 24 | end 25 | 26 | def timer 27 | Timer.new( 28 | @relevant_commits.all, Heartbeat 29 | ).process 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Russell Osborne 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /lib/gitwakatime/durations_calculator.rb: -------------------------------------------------------------------------------- 1 | module GitWakaTime 2 | # Extract Duration Data from Heartbeats for the WAKATIME API 3 | class DurationsCalculator 4 | attr_accessor :heartbeats 5 | def initialize(args) 6 | return @heartbeats = args[:heartbeats] if args[:heartbeats] 7 | @args = args 8 | @heartbeats = [] 9 | end 10 | 11 | def heartbeats_to_durations(timeout = 15) 12 | durations = [] 13 | current = nil 14 | @heartbeats.each do |heartbeat| 15 | # the first heartbeat just sets state and does nothing 16 | unless current.nil? 17 | 18 | # get duration since last heartbeat 19 | duration = heartbeat.time.round - current.time.round 20 | 21 | duration = 0.0 if duration < 0 22 | 23 | # duration not logged if greater than the timeout 24 | if duration < timeout * 60 25 | 26 | # add duration to current heartbeat 27 | current.duration = duration 28 | 29 | # save to local db 30 | current.save 31 | 32 | # log current heartbeat as a duration 33 | durations << current 34 | end 35 | end 36 | # set state (re-start the clock) 37 | current = heartbeat 38 | end 39 | durations 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/gitwakatime/models/commit.rb: -------------------------------------------------------------------------------- 1 | module GitWakaTime 2 | ## 3 | # Cache git commit and correlate it's children 4 | # 5 | class Commit < Sequel::Model 6 | one_to_many :commited_files 7 | def after_create 8 | extract_changed_files if GitWakaTime.config.git 9 | end 10 | 11 | def to_s 12 | format(' %-8s %8s %-30s %-80s'.green, 13 | sha[0..8], 14 | date, 15 | ChronicDuration.output(time_in_seconds.to_i), 16 | message 17 | ) 18 | end 19 | 20 | def oldest_dependent 21 | @files.sort { |f| f.commit.date }.first 22 | end 23 | 24 | def time_in_seconds 25 | commited_files.map(&:time_in_seconds).compact.inject(:+) 26 | end 27 | 28 | def date 29 | self[:date].localtime 30 | end 31 | 32 | private 33 | 34 | def extract_changed_files 35 | @raw_commit = GitWakaTime.config.git.gcommit(sha) 36 | # TODO: Assume gap time to lookup time prior to first commit. 37 | return unless @raw_commit.parent 38 | update(parent_sha: @raw_commit.parent.sha) 39 | 40 | @raw_commit.diff_parent.stats[:files].keys.map do |file| 41 | CommitedFile.find_or_create(commit_id: id, name: file) do |c| 42 | c.update(sha: sha, project: project) 43 | end 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/dummy/dot_git/hooks/prepare-commit-msg.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to prepare the commit log message. 4 | # Called by "git commit" with the name of the file that has the 5 | # commit message, followed by the description of the commit 6 | # message's source. The hook's purpose is to edit the commit 7 | # message file. If the hook fails with a non-zero status, 8 | # the commit is aborted. 9 | # 10 | # To enable this hook, rename this file to "prepare-commit-msg". 11 | 12 | # This hook includes three examples. The first comments out the 13 | # "Conflicts:" part of a merge commit. 14 | # 15 | # The second includes the output of "git diff --name-status -r" 16 | # into the message, just before the "git status" output. It is 17 | # commented because it doesn't cope with --amend or with squashed 18 | # commits. 19 | # 20 | # The third example adds a Signed-off-by line to the message, that can 21 | # still be edited. This is rarely a good idea. 22 | 23 | case "$2,$3" in 24 | merge,) 25 | /usr/bin/perl -i.bak -ne 's/^/# /, s/^# #/#/ if /^Conflicts/ .. /#/; print' "$1" ;; 26 | 27 | # ,|template,) 28 | # /usr/bin/perl -i.bak -pe ' 29 | # print "\n" . `git diff --cached --name-status -r` 30 | # if /^#/ && $first++ == 0' "$1" ;; 31 | 32 | *) ;; 33 | esac 34 | 35 | # SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') 36 | # grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" 37 | -------------------------------------------------------------------------------- /gitwakatime.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'gitwakatime/version' 5 | 6 | Gem::Specification.new do |s| 7 | s.name = 'gitwakatime' 8 | s.version = GitWakaTime::VERSION 9 | s.authors = ['Russell Osborne'] 10 | s.email = ['russosborn@gmail.com'] 11 | s.summary = 'A Tool that will compile git data with wakatime 12 | data to establish time per commit' 13 | s.description = 'A Tool that will compile git data with wakatime 14 | data to establish time per commit ' 15 | s.homepage = '' 16 | s.license = 'MIT' 17 | 18 | s.files = `git ls-files`.split($RS) 19 | s.test_files = s.files.grep(/^spec\//) 20 | s.executables = s.files.grep(/^bin\//) { |f| File.basename(f) } 21 | 22 | s.require_paths = ['lib'] 23 | s.authors = ['Russell Osborne'] 24 | s.add_runtime_dependency 'git', '>= 1.2.9.1' 25 | s.add_runtime_dependency 'wakatime', '>= 0.0.2' 26 | s.add_runtime_dependency 'thor', '>= 0' 27 | s.add_runtime_dependency 'chronic_duration', '>=0' 28 | s.add_runtime_dependency 'colorize' 29 | s.add_runtime_dependency('activesupport', ['>= 0']) 30 | s.add_runtime_dependency 'sequel' 31 | s.add_runtime_dependency 'sqlite3' 32 | s.add_development_dependency('bundler', ['>= 0']) 33 | s.add_development_dependency 'rake' 34 | s.add_development_dependency 'rspec' 35 | s.add_development_dependency('webmock', ['>= 0']) 36 | s.add_development_dependency('pry', ['>= 0']) 37 | end 38 | -------------------------------------------------------------------------------- /spec/dummy/dot_git/hooks/pre-push.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # An example hook script to verify what is about to be pushed. Called by "git 4 | # push" after it has checked the remote status, but before anything has been 5 | # pushed. If this script exits with a non-zero status nothing will be pushed. 6 | # 7 | # This hook is called with the following parameters: 8 | # 9 | # $1 -- Name of the remote to which the push is being done 10 | # $2 -- URL to which the push is being done 11 | # 12 | # If pushing without using a named remote those arguments will be equal. 13 | # 14 | # Information about the commits which are being pushed is supplied as lines to 15 | # the standard input in the form: 16 | # 17 | # 18 | # 19 | # This sample shows how to prevent push of commits where the log message starts 20 | # with "WIP" (work in progress). 21 | 22 | remote="$1" 23 | url="$2" 24 | 25 | z40=0000000000000000000000000000000000000000 26 | 27 | IFS=' ' 28 | while read local_ref local_sha remote_ref remote_sha 29 | do 30 | if [ "$local_sha" = $z40 ] 31 | then 32 | # Handle delete 33 | : 34 | else 35 | if [ "$remote_sha" = $z40 ] 36 | then 37 | # New branch, examine all commits 38 | range="$local_sha" 39 | else 40 | # Update to existing branch, examine new commits 41 | range="$remote_sha..$local_sha" 42 | fi 43 | 44 | # Check for WIP commit 45 | commit=`git rev-list -n 1 --grep '^WIP' "$range"` 46 | if [ -n "$commit" ] 47 | then 48 | echo >&2 "Found WIP commit in $local_ref, not pushing" 49 | exit 1 50 | fi 51 | fi 52 | done 53 | 54 | exit 0 55 | -------------------------------------------------------------------------------- /lib/gitwakatime/query.rb: -------------------------------------------------------------------------------- 1 | require 'benchmark' 2 | require 'colorize' 3 | 4 | module GitWakaTime 5 | # Integrates the nested hash from mapper with heartbeats api 6 | class Query 7 | def initialize(start_at, end_at, project) 8 | @start_at = start_at.to_time 9 | @end_at = end_at.to_time 10 | @project = project 11 | @requests = RequestBuilder.new(@start_at, @end_at).call 12 | Log.new "Loading Committed time from #{@start_at} to #{@end_at}".red 13 | end 14 | 15 | def call 16 | @requests.each do |params| 17 | next if cached?(params[:date]) 18 | persist_heartbeats_localy(Request.new(params).call) 19 | end 20 | 21 | DurationsCalculator.new(heartbeats: local_heartbeats.where((Sequel[:duration] <= 0)) ).heartbeats_to_durations 22 | local_heartbeats.where(project: @project).all 23 | end 24 | 25 | def cached?(date) 26 | max_local_timestamp = Heartbeat.max(:time) 27 | return false if max_local_timestamp.nil? 28 | @max_local_timestamp ||= (Time.parse(max_local_timestamp + ' UTC')) 29 | date.to_date < @max_local_timestamp.to_date 30 | end 31 | 32 | private 33 | 34 | def persist_heartbeats_localy(heartbeats) 35 | heartbeats.map do |heartbeat| 36 | heartbeat['uuid'] = heartbeat['id'] 37 | heartbeat['time'] = Time.at(heartbeat['time']) 38 | heartbeat.delete('id') 39 | Heartbeat.find_or_create(uuid: heartbeat['uuid']) do |a| 40 | a.update(heartbeat) 41 | end 42 | end 43 | end 44 | 45 | def local_heartbeats 46 | Heartbeat.where(time: @start_at..@end_at) 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/query_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe GitWakaTime::Query do 4 | 5 | before(:each) do 6 | GitWakaTime.config.git = Git.open(@wdir) 7 | GitWakaTime::Mapper.new(start_at: Date.new(2015, 1, 24)) 8 | end 9 | 10 | let(:start_at) { Date.new(2015, 1, 24) } 11 | let(:end_at) { Date.new(2015, 2, 24) } 12 | subject(:subject) { described_class.new(start_at, end_at, File.basename(@wdir)) } 13 | 14 | before do 15 | stub_request(:get, /.*wakatime.com\/api\/v1\/users\/current\/heartbeats/) 16 | .with(query: hash_including(:date)) 17 | .to_return(body: File.read('./spec/fixtures/heartbeats.json'), status: 200) 18 | end 19 | 20 | it 'will return an array of heartbeats' do 21 | heartbeats = subject.call 22 | 23 | expect(heartbeats).to be_a Array 24 | expect(heartbeats.size).to eq 9 # 10ths is lonely 25 | expect(heartbeats.last).to be_a GitWakaTime::Heartbeat 26 | expect(heartbeats.last.branch).to eq 'master' 27 | end 28 | 29 | describe 'caching' do 30 | it 'when heartbeats after query date return true ' do 31 | expect(GitWakaTime::Heartbeat).to receive(:max).with(:time) 32 | .and_return(Time.now.to_s) 33 | expect(subject.cached?(1.month.ago)).to eq(true) 34 | end 35 | 36 | it 'when heartbeats after query date are the same ' do 37 | expect(GitWakaTime::Heartbeat).to receive(:max).with(:time) 38 | .and_return(Time.now.to_s) 39 | 40 | expect(subject.cached?(Date.today)).to eq(false) 41 | end 42 | 43 | it 'when no heartbeats present' do 44 | expect(GitWakaTime::Heartbeat).to receive(:max).with(:time) 45 | .and_return(nil) 46 | 47 | expect(subject.cached?(Date.today)).to eq(false) 48 | end 49 | 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/dummy/dot_git/hooks/pre-commit.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to verify what is about to be committed. 4 | # Called by "git commit" with no arguments. The hook should 5 | # exit with non-zero status after issuing an appropriate message if 6 | # it wants to stop the commit. 7 | # 8 | # To enable this hook, rename this file to "pre-commit". 9 | 10 | if git rev-parse --verify HEAD >/dev/null 2>&1 11 | then 12 | against=HEAD 13 | else 14 | # Initial commit: diff against an empty tree object 15 | against=4b825dc642cb6eb9a060e54bf8d69288fbee4904 16 | fi 17 | 18 | # If you want to allow non-ASCII filenames set this variable to true. 19 | allownonascii=$(git config --bool hooks.allownonascii) 20 | 21 | # Redirect output to stderr. 22 | exec 1>&2 23 | 24 | # Cross platform projects tend to avoid non-ASCII filenames; prevent 25 | # them from being added to the repository. We exploit the fact that the 26 | # printable range starts at the space character and ends with tilde. 27 | if [ "$allownonascii" != "true" ] && 28 | # Note that the use of brackets around a tr range is ok here, (it's 29 | # even required, for portability to Solaris 10's /usr/bin/tr), since 30 | # the square bracket bytes happen to fall in the designated range. 31 | test $(git diff --cached --name-only --diff-filter=A -z $against | 32 | LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 33 | then 34 | cat <<\EOF 35 | Error: Attempt to add a non-ASCII file name. 36 | 37 | This can cause problems if you want to work with people on other platforms. 38 | 39 | To be portable it is advisable to rename the file. 40 | 41 | If you know what you are doing you can disable this check using: 42 | 43 | git config hooks.allownonascii true 44 | EOF 45 | exit 1 46 | fi 47 | 48 | # If there are whitespace errors, print the offending file names and fail. 49 | exec git diff-index --check --cached $against -- 50 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file was generated by the `rspec --init` command. Conventionally, all 2 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 3 | # Require this file using `require "spec_helper"` to ensure that it is only 4 | # loaded once. 5 | # 6 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 7 | ENV['thor_env'] = 'test' 8 | ENV['waka_log'] = 'false' 9 | require 'simplecov' 10 | SimpleCov.start 11 | 12 | require 'webmock/rspec' 13 | WebMock.disable_net_connect!(allow: 'codeclimate.com') 14 | require 'gitwakatime' 15 | 16 | RSpec.configure do |config| 17 | config.run_all_when_everything_filtered = true 18 | config.filter_run :focus 19 | config.order = 'random' 20 | config.before(:all) do 21 | @wdir = set_file_paths 22 | GitWakaTime.config.setup_local_db 23 | 24 | require_relative '../lib/gitwakatime/models/heartbeat' 25 | require_relative '../lib/gitwakatime/models/commit' 26 | require_relative '../lib/gitwakatime/models/commited_file' 27 | 28 | GitWakaTime::Commit.new.columns 29 | GitWakaTime::CommitedFile.new.columns 30 | GitWakaTime::Heartbeat.new.columns 31 | end 32 | 33 | config.around(:each) do |example| 34 | DB.transaction(rollback: :always, auto_savepoint: true) { example.run } 35 | end 36 | 37 | config.before(:each) do 38 | allow( 39 | GitWakaTime.config 40 | ).to receive('user_name').and_return('Russell Osborne') 41 | end 42 | 43 | config.after(:all) do 44 | FileUtils.rm_r(File.dirname(@wdir)) 45 | end 46 | end 47 | 48 | def set_file_paths 49 | @test_dir = File.join(File.dirname(__FILE__)) 50 | @wdir_dot = File.expand_path(File.join(@test_dir, 'dummy')) 51 | @wdir = create_temp_repo(@wdir_dot) 52 | end 53 | 54 | def create_temp_repo(clone_path) 55 | filename = 'git_test' + Time.now.to_i.to_s + rand(300).to_s.rjust(3, '0') 56 | @tmp_path = File.expand_path( 57 | File.join(File.dirname(__FILE__), '..', 'tmp', filename) 58 | ) 59 | FileUtils.mkdir_p(@tmp_path) 60 | FileUtils.cp_r(clone_path, @tmp_path) 61 | tmp_path = File.join(@tmp_path, 'dummy') 62 | activate_repo_as_git(tmp_path) 63 | tmp_path 64 | end 65 | 66 | def activate_repo_as_git(path) 67 | Dir.chdir(path) do 68 | FileUtils.mv('dot_git', '.git') 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/gitwakatime/models/commited_file.rb: -------------------------------------------------------------------------------- 1 | module GitWakaTime 2 | ## 3 | # Determines When a file was lasted commit and stores time and hash. 4 | class CommitedFile < Sequel::Model 5 | many_to_one :commit 6 | 7 | # No two committed files should have the same name + dependent_date this 8 | # means a split tree, and we should split time between the two, or 9 | # more, commits. 10 | def before_create 11 | find_dependent_commit(name) if GitWakaTime.config.git 12 | end 13 | 14 | def to_s 15 | format(' %-20s %-40s %-100s '.blue, 16 | (dependent_sha[0..8] if dependent_sha), 17 | ChronicDuration.output(time_in_seconds.to_f), 18 | name 19 | ) 20 | end 21 | 22 | private 23 | 24 | # Call git log for path, loop through till we find a valid commit or run 25 | # out of commits to check 26 | def find_dependent_commit(name, i = 1) 27 | commits = load_dependent_commits(name) 28 | loop do 29 | commit = commits[i] 30 | 31 | if commit && allowed_commit(commit) 32 | set dependent_sha: commit.sha, dependent_date: commit.date 33 | check_and_correct_split_tree(commit) 34 | end 35 | 36 | i += 1 37 | break if !dependent_sha.nil? || commit.nil? 38 | end 39 | end 40 | 41 | def check_and_correct_split_tree(commit) 42 | # This is the magical fix for the split tree issue 43 | # Current though is this will fail if more than 2 split tree files 44 | split_tree_file = CommitedFile.where( 45 | name: name, dependent_sha: commit.sha 46 | ).first 47 | return unless split_tree_file && split_tree_file.commit 48 | 49 | if self.commit.date < split_tree_file.commit.date 50 | self.dependent_date = split_tree_file.commit.date 51 | elsif self.commit.date > split_tree_file.commit.date 52 | split_tree_file.update(dependent_date: commit.date.utc) 53 | end 54 | end 55 | 56 | def allowed_commit(commit) 57 | return false if commit.sha == sha 58 | return false if commit.author.name != GitWakaTime.config.user_name 59 | return false if commit.parents.size > 1 60 | true 61 | end 62 | 63 | def load_dependent_commits(name) 64 | GitWakaTime.config.git.log.object(sha).path(name) 65 | rescue Git::GitExecuteError 66 | puts error 67 | nil 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/gitwakatime/timer.rb: -------------------------------------------------------------------------------- 1 | require 'benchmark' 2 | require 'colorize' 3 | require 'active_support' 4 | require 'active_support/core_ext/time' 5 | 6 | module GitWakaTime 7 | # Integrates the nested hash from mapper with heartbeats api 8 | class Timer 9 | def initialize(commits, heartbeats_with_durations) 10 | @commits = commits 11 | @heartbeats_with_durations = heartbeats_with_durations 12 | end 13 | 14 | def total 15 | total_time = sum_heartbeats @heartbeats_with_durations 16 | Log.new "Total Recorded time #{ChronicDuration.output total_time.to_f}", :red 17 | end 18 | 19 | def total_commited 20 | total_commited = ChronicDuration.output(@commits_with_duration 21 | .map(&:time_in_seconds).compact 22 | .reduce(:+).to_f) 23 | Log.new "Total Committed Time #{total_commited} ".red 24 | end 25 | 26 | def process 27 | @commits_with_duration = @commits.each do |commit| 28 | if commit.commited_files.count > 0 || commit.parent_sha 29 | commit.commited_files.each_with_index do |file, _i| 30 | time = sum_heartbeats relevant_heartbeats(commit, file) 31 | file.time_in_seconds = time 32 | commit.time_in_seconds = time 33 | 34 | file.save 35 | end 36 | commit.save 37 | else 38 | commit.time_in_seconds = sum_heartbeats( 39 | heartbeats_before(@heartbeats_with_durations, commit.date) 40 | ) 41 | end 42 | end.compact 43 | total 44 | total_commited 45 | @commits_with_duration.group_by { |c| c.date.to_date } 46 | end 47 | 48 | private 49 | 50 | def relevant_heartbeats(commit, file) 51 | # The file should be the same file as we expect 52 | heartbeats = @heartbeats_with_durations.grep(:entity, "%#{file.name}%") 53 | 54 | # The timestamps should be before the expected commit 55 | heartbeats = heartbeats_before(heartbeats, commit.date) 56 | 57 | # If this file had an earlier commit ensure the heartbeats timestamp 58 | # is after that commit 59 | if file.dependent_date 60 | heartbeats = heartbeats_after(heartbeats, file.dependent_date) 61 | end 62 | heartbeats 63 | end 64 | 65 | def heartbeats_before(heartbeats, date) 66 | heartbeats.where(Sequel[:time] <= date) 67 | end 68 | 69 | def heartbeats_after(heartbeats, date) 70 | heartbeats.where(Sequel[:time] >= date) 71 | end 72 | 73 | def sum_heartbeats(heartbeats) 74 | heartbeats.sum(:duration) 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /spec/timer_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'description' do 4 | let(:git) { Git.open(@wdir) } 5 | let(:heartbeat) { double(:heartbeat, sum: 1) } 6 | let(:heartbeats) { double(:heartbeats, grep: [heartbeat, heartbeat]) } 7 | 8 | before do 9 | stub_request(:get, /.*wakatime.com\/api\/v1\/heartbeats/) 10 | .with(query: hash_including(:date)) 11 | .to_return(body: File.read('./spec/fixtures/heartbeats.json'), status: 200) 12 | 13 | # Prevent any callbacks to git. 14 | # TODO: Refactor callbacks. 15 | GitWakaTime.config.git = nil 16 | end 17 | 18 | it 'can be run on dummy with no heartbeats' do 19 | timer = GitWakaTime::Timer.new( 20 | GitWakaTime::Commit.all, GitWakaTime::Heartbeat 21 | ).process 22 | 23 | # # UTC breaks heartbeats of 1 day 24 | # expect(timer.size).to eq 1 25 | # With 7 relevant commits 26 | expect(timer).to eq Hash.new 27 | end 28 | 29 | it 'can be run on dummy with no heartbeats' do 30 | c1 = GitWakaTime::Commit.create(sha: 'e493d6f2ab2a702fa7f9c168b852a3b44c524f08', parent_sha: '4c1ea35f9a811a0ef79da15ec85f25fce4c446ba', project: 'dummy', time_in_seconds: 0, date: Time.parse('2015-01-30 03:26:12 UTC'), message: 'conflicting commit on master.', author: 'Russell Osborne') 31 | c2 = GitWakaTime::Commit.create(sha: 'd642b3c04c3025655a9c33e32b9d530696dcf7cc', parent_sha: '4c1ea35f9a811a0ef79da15ec85f25fce4c446ba', project: 'dummy', time_in_seconds: 0, date: Time.parse('2015-01-30 03:26:05 UTC'), message: 'another commit on dev.', author: 'Russell Osborne') 32 | GitWakaTime::CommitedFile.create(commit_id: c1.id, dependent_sha: '4c1ea35f9a811a0ef79da15ec85f25fce4c446ba', dependent_date: Time.parse('2015-01-30 01:00:00 UTC'), time_in_seconds: 0, sha: 'd642b3c04c3025655a9c33e32b9d530696dcf7cc', name: '/dummy/spec/commit_spec.rb', entity: nil, project: 'dummy') 33 | GitWakaTime::CommitedFile.create(commit_id: c2.id, dependent_sha: '4c1ea35f9a811a0ef79da15ec85f25fce4c446ba', dependent_date: Time.parse('2015-01-30 02:00:00 UTC'), time_in_seconds: 0, sha: 'd642b3c04c3025655a9c33e32b9d530696dcf7cc', name: '/dummy/lib/dummy/timer.rb', entity: nil, project: 'dummy') 34 | GitWakaTime::Heartbeat.create(time: Time.parse('2015-01-30 02:33:00 UTC'), duration: 500, entity: '/dummy/spec/commit_spec.rb', type: 'file', branch: 'master', project: 'dummy') 35 | GitWakaTime::Heartbeat.create(time: Time.parse('2015-01-30 02:31:54 UTC'), duration: 333, entity: '/dummy/lib/dummy/timer.rb', type: 'file', branch: 'master', project: 'dummy') 36 | 37 | timer = GitWakaTime::Timer.new( 38 | GitWakaTime::Commit.all, GitWakaTime::Heartbeat 39 | ).process 40 | 41 | # One hash key per day 42 | expect(timer.size).to eq 1 43 | expect(timer[Date.new(2015, 1, 30)]).to be_a(Array) 44 | expect(timer[Date.new(2015, 1, 30)].map(&:time_in_seconds).compact.reduce(&:+)).to eq(500 + 333) 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/gitwakatime.rb: -------------------------------------------------------------------------------- 1 | require 'sequel' 2 | 3 | Sequel::Model.plugin :json_serializer 4 | Sequel.default_timezone = :utc 5 | 6 | if ENV['thor_env'] != 'test' 7 | DB = Sequel.connect("sqlite://#{File.join(Dir.home, '.wakatime.sqlite')}") 8 | else 9 | # Use a in memory db to have a nice clean testing bed. 10 | DB = Sequel.sqlite 11 | end 12 | 13 | DB.use_timestamp_timezones = false 14 | 15 | 16 | require_relative './gitwakatime/version' 17 | require_relative './gitwakatime/durations_calculator' 18 | require_relative './gitwakatime/mapper' 19 | require_relative './gitwakatime/query' 20 | require_relative './gitwakatime/request' 21 | require_relative './gitwakatime/request_builder' 22 | require_relative './gitwakatime/timer' 23 | require_relative './gitwakatime/log' 24 | require_relative './gitwakatime/controller' 25 | require_relative './gitwakatime/cli' 26 | 27 | 28 | # It's a module :) 29 | module GitWakaTime 30 | ## 31 | # Stores primary config and project information 32 | # Currently not thread safe. 33 | class Configuration 34 | attr_accessor :api_key, :log_level, :root, :project, :git 35 | 36 | def initialize 37 | self.api_key = nil 38 | self.log_level = :info 39 | end 40 | 41 | def user_name 42 | GitWakaTime.config.git.config('user.name') 43 | end 44 | 45 | def load_config_yaml 46 | yaml = YAML.load_file(File.join(Dir.home, '.wakatime.yml')) 47 | self.api_key = yaml[:api_key] 48 | self.log_level = yaml[:log_level] 49 | end 50 | 51 | def setup_local_db 52 | create_commits_table 53 | create_commited_files_table 54 | create_heartbeats_table 55 | end 56 | 57 | def create_commits_table 58 | DB.create_table? :commits do 59 | primary_key :id 60 | String :sha 61 | String :parent_sha 62 | String :project 63 | integer :time_in_seconds, default: 0 64 | datetime :date 65 | text :message 66 | String :author 67 | end 68 | end 69 | 70 | def create_commited_files_table 71 | DB.create_table? :commited_files do 72 | primary_key :id 73 | integer :commit_id 74 | String :dependent_sha 75 | DateTime :dependent_date 76 | integer :time_in_seconds, default: 0 77 | String :sha 78 | String :name 79 | String :entity 80 | String :project 81 | index :dependent_sha 82 | index :sha 83 | end 84 | end 85 | 86 | def create_heartbeats_table 87 | DB.create_table? :heartbeats do 88 | primary_key :id 89 | String :uuid 90 | DateTime :time 91 | integer :duration, default: 0 92 | String :entity 93 | String :type 94 | String :branch 95 | String :project 96 | index :uuid, unique: true 97 | end 98 | end 99 | end 100 | 101 | def self.config 102 | @configuration ||= Configuration.new 103 | end 104 | 105 | def self.configure 106 | yield(configuration) if block_given? 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /lib/gitwakatime/cli.rb: -------------------------------------------------------------------------------- 1 | require 'git' 2 | require 'logger' 3 | require 'wakatime' 4 | require 'chronic_duration' 5 | require 'yaml' 6 | require 'thor' 7 | require 'active_support' 8 | require 'active_support/core_ext/date/calculations' 9 | require 'active_support/core_ext/date_and_time/calculations' 10 | require 'active_support/core_ext/integer/time' 11 | require 'active_support/core_ext/time' 12 | 13 | module GitWakaTime 14 | # Provides two CLI heartbeats init and tally 15 | class Cli < Thor 16 | include Thor::Actions 17 | desc 'init', 'Setups up Project for using the wakatime API 18 | it will also add to your git ignore file' 19 | method_option :file, aliases: '-f', default: '.' 20 | 21 | def init 22 | unless File.exist?(File.join(Dir.home, '.wakatime.yml')) 23 | api_key = ask('What is your wakatime api key? ( Get it here https://wakatime.com/settings):') 24 | say('Adding .wakatime.yml to home directory') 25 | 26 | create_file File.join(Dir.home, '.wakatime.yml') do 27 | YAML.dump(api_key: api_key, last_commit: nil, log_level: :info) 28 | end 29 | end 30 | reset 31 | end 32 | 33 | desc 'reset', 'Reset local sqlite db' 34 | def reset 35 | DB.disconnect 36 | 37 | db_path = File.expand_path(File.join(Dir.home, '.wakatime.sqlite')) 38 | FileUtils.rm_r(db_path) if File.exist?(db_path) 39 | DB.connect("sqlite://#{db_path}") 40 | GitWakaTime.config.setup_local_db 41 | end 42 | 43 | desc 'tally', 'Produce time spend for each commit and file in each commit' 44 | method_option :file, aliases: '-f', default: '.' 45 | method_option :start_on, aliases: '-s', default: 7.days.ago.to_s 46 | method_option :output, aliases: '-o', default: 'text', type: 'string' 47 | 48 | def tally 49 | date = Date.parse(options.start_on) 50 | 51 | require_relative 'models/heartbeat' 52 | require_relative 'models/commit' 53 | require_relative 'models/commited_file' 54 | 55 | @timer = GitWakaTime::Controller.new( 56 | path: File.expand_path(options.file), date: date 57 | ).timer 58 | 59 | print_output(@timer, format: options.output) 60 | end 61 | 62 | desc 'update', 'Cache the latest heartbeats locally' 63 | method_option :start_on, aliases: '-s', default: Date.today - 7 64 | def update 65 | GitWakaTime.config.setup_local_db 66 | GitWakaTime.config.load_config_yaml 67 | 68 | require_relative 'models/heartbeat' 69 | require_relative 'models/commit' 70 | require_relative 'models/commited_file' 71 | 72 | date = Date.parse(options.start_on || GitWakaTime::Heartbeat.max(:time)) 73 | 74 | GitWakaTime::Query.new(date, Date.today, @project).call 75 | end 76 | 77 | no_commands do 78 | def print_output(timer, format: 'text') 79 | if format == 'text' 80 | timer.each do |c_date, commits| 81 | print_commit(c_date, commits) 82 | end 83 | elsif format == 'json' 84 | @timer.to_json 85 | end 86 | end 87 | 88 | def print_commit(c_date, commits) 89 | sum_c_time = commits.map(&:time_in_seconds).compact.reduce(&:+).to_i 90 | Log.new format( 91 | '%-40s %-40s'.blue, 92 | c_date, 93 | "Total #{ChronicDuration.output sum_c_time}" 94 | ) 95 | commits.each do |commit| 96 | # Log.new commit.message 97 | Log.new commit.to_s 98 | commit.commited_files.each { |file| Log.new file.to_s } 99 | end 100 | end 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /spec/commited_file_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | # * commit a4c26aeb79acb1f012201fe96e4d68e8d17c75d9 3 | # | (HEAD, origin/master, origin/HEAD, master) 4 | # | Author: rpo 5 | # | Date: Sat Jan 31 15:49:07 2015 -0500 6 | # | 7 | # | I was edited online using git hub's editor 8 | # | 9 | # * commit 570f1df0505ed828656eeaf9411ddd6a6068b095 10 | # | Author: Russell Osborne 11 | # | Date: Fri Jan 30 00:19:00 2015 -0500 12 | # | 13 | # | And one more 14 | # | 15 | # * commit dcd748bd06b8a0f239d779bee4f1eaf1f4aa500d 16 | # | Author: Russell Osborne 17 | # | Date: Fri Jan 30 00:01:54 2015 -0500 18 | # | 19 | # | a final commit on master 20 | # | 21 | # * commit 57b0d5cdb6da2b2b9ac7e9847716b0c54466d1c6 22 | # |\ Merge: e493d6f d642b3c 23 | # | | Author: Russell Osborne 24 | # | | Date: Thu Jan 29 22:27:26 2015 -0500 25 | # | | 26 | # | | Merge branch 'dev' 27 | # | | 28 | # | * commit d642b3c04c3025655a9c33e32b9d530696dcf7cc 29 | # | | Author: Russell Osborne 30 | # | | Date: Thu Jan 29 22:26:05 2015 -0500 31 | # | | 32 | # | | another commit on dev. 33 | # | | 34 | # * | commit e493d6f2ab2a702fa7f9c168b852a3b44c524f08 35 | # |/ Author: Russell Osborne 36 | # | Date: Thu Jan 29 22:26:20 2015 -0500 or 1422570380 37 | # | 38 | # | conflicting commit on master. 39 | # | 40 | # * commit 4c1ea35f9a811a0ef79da15ec85f25fce4c446ba 41 | # | Author: Russell Osborne 42 | # | Date: Thu Jan 29 22:25:08 2015 -0500 or 1422570308 43 | # | 44 | # | commit on dev branch 45 | # | 46 | # * commit 2254dd56976b5f32a2289438842e42a35a18ff86 47 | # | Author: Russell Osborne 48 | # | Date: Thu Jan 29 21:49:31 2015 -0500 49 | # | 50 | # | testing 51 | # | 52 | # * commit 052ff8c0e8c7cd39880d1536f4e27cc554e698f6 53 | # Author: Russell Osborne 54 | # Date: Thu Jan 29 21:49:12 2015 -0500 55 | 56 | # created readme 57 | describe 'description' do 58 | let(:git) { Git.open(@wdir) } 59 | 60 | before do 61 | GitWakaTime::Commit.dataset.destroy 62 | GitWakaTime::Commit.dataset.destroy 63 | end 64 | 65 | it 'can be created ' do 66 | GitWakaTime.config.git = git 67 | 68 | first_commit = GitWakaTime::Commit.find_or_create( 69 | sha: 'e493d6f2ab2a702fa7f9c168b852a3b44c524f08', 70 | author: 'Russell Osborne', 71 | message: 'conflicting commit on blah.', 72 | project: git.repo.to_s, 73 | date: DateTime.parse('Thu Jan 29 22:26:20 2015 -0500').utc 74 | ) 75 | 76 | expect( 77 | first_commit.commited_files.first.dependent_sha 78 | ).to eql( 79 | '4c1ea35f9a811a0ef79da15ec85f25fce4c446ba' 80 | ) 81 | expect( 82 | first_commit.commited_files.first.dependent_date.utc.to_s 83 | ).to eql( 84 | '2015-01-30 03:25:08 UTC' 85 | ) 86 | 87 | second_commit = GitWakaTime::Commit.find_or_create( 88 | sha: 'd642b3c04c3025655a9c33e32b9d530696dcf7cc', 89 | author: 'Russell Osborne', 90 | message: 'conflicting commit on master.', 91 | project: git.repo.to_s, 92 | date: DateTime.parse('Thu Jan 29 22:26:05 2015 -0500').utc 93 | ) 94 | 95 | # Verify that we have a split tree, both commit 1 and two should depend on 96 | # 4c1ea 97 | expect( 98 | second_commit.commited_files.first.dependent_sha 99 | ).to eql('4c1ea35f9a811a0ef79da15ec85f25fce4c446ba') 100 | 101 | expect( 102 | GitWakaTime::Commit.find(id: first_commit.id) 103 | .commited_files.first.dependent_date.utc.to_s 104 | ).to eql('2015-01-30 03:25:08 UTC') 105 | 106 | # Second commit should depend on first commit not actual depedent time as 107 | # it would duplicate time 108 | expect( 109 | second_commit.commited_files.first.dependent_date.utc.to_s 110 | ).to eql(Time.parse('Thu Jan 29 22:26:20 2015 -0500').utc.to_s) 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /spec/dummy/dot_git/hooks/update.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to blocks unannotated tags from entering. 4 | # Called by "git receive-pack" with arguments: refname sha1-old sha1-new 5 | # 6 | # To enable this hook, rename this file to "update". 7 | # 8 | # Config 9 | # ------ 10 | # hooks.allowunannotated 11 | # This boolean sets whether unannotated tags will be allowed into the 12 | # repository. By default they won't be. 13 | # hooks.allowdeletetag 14 | # This boolean sets whether deleting tags will be allowed in the 15 | # repository. By default they won't be. 16 | # hooks.allowmodifytag 17 | # This boolean sets whether a tag may be modified after creation. By default 18 | # it won't be. 19 | # hooks.allowdeletebranch 20 | # This boolean sets whether deleting branches will be allowed in the 21 | # repository. By default they won't be. 22 | # hooks.denycreatebranch 23 | # This boolean sets whether remotely creating branches will be denied 24 | # in the repository. By default this is allowed. 25 | # 26 | 27 | # --- Command line 28 | refname="$1" 29 | oldrev="$2" 30 | newrev="$3" 31 | 32 | # --- Safety check 33 | if [ -z "$GIT_DIR" ]; then 34 | echo "Don't run this script from the command line." >&2 35 | echo " (if you want, you could supply GIT_DIR then run" >&2 36 | echo " $0 )" >&2 37 | exit 1 38 | fi 39 | 40 | if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then 41 | echo "usage: $0 " >&2 42 | exit 1 43 | fi 44 | 45 | # --- Config 46 | allowunannotated=$(git config --bool hooks.allowunannotated) 47 | allowdeletebranch=$(git config --bool hooks.allowdeletebranch) 48 | denycreatebranch=$(git config --bool hooks.denycreatebranch) 49 | allowdeletetag=$(git config --bool hooks.allowdeletetag) 50 | allowmodifytag=$(git config --bool hooks.allowmodifytag) 51 | 52 | # check for no description 53 | projectdesc=$(sed -e '1q' "$GIT_DIR/description") 54 | case "$projectdesc" in 55 | "Unnamed repository"* | "") 56 | echo "*** Project description file hasn't been set" >&2 57 | exit 1 58 | ;; 59 | esac 60 | 61 | # --- Check types 62 | # if $newrev is 0000...0000, it's a commit to delete a ref. 63 | zero="0000000000000000000000000000000000000000" 64 | if [ "$newrev" = "$zero" ]; then 65 | newrev_type=delete 66 | else 67 | newrev_type=$(git cat-file -t $newrev) 68 | fi 69 | 70 | case "$refname","$newrev_type" in 71 | refs/tags/*,commit) 72 | # un-annotated tag 73 | short_refname=${refname##refs/tags/} 74 | if [ "$allowunannotated" != "true" ]; then 75 | echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2 76 | echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2 77 | exit 1 78 | fi 79 | ;; 80 | refs/tags/*,delete) 81 | # delete tag 82 | if [ "$allowdeletetag" != "true" ]; then 83 | echo "*** Deleting a tag is not allowed in this repository" >&2 84 | exit 1 85 | fi 86 | ;; 87 | refs/tags/*,tag) 88 | # annotated tag 89 | if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1 90 | then 91 | echo "*** Tag '$refname' already exists." >&2 92 | echo "*** Modifying a tag is not allowed in this repository." >&2 93 | exit 1 94 | fi 95 | ;; 96 | refs/heads/*,commit) 97 | # branch 98 | if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then 99 | echo "*** Creating a branch is not allowed in this repository" >&2 100 | exit 1 101 | fi 102 | ;; 103 | refs/heads/*,delete) 104 | # delete branch 105 | if [ "$allowdeletebranch" != "true" ]; then 106 | echo "*** Deleting a branch is not allowed in this repository" >&2 107 | exit 1 108 | fi 109 | ;; 110 | refs/remotes/*,commit) 111 | # tracking branch 112 | ;; 113 | refs/remotes/*,delete) 114 | # delete tracking branch 115 | if [ "$allowdeletebranch" != "true" ]; then 116 | echo "*** Deleting a tracking branch is not allowed in this repository" >&2 117 | exit 1 118 | fi 119 | ;; 120 | *) 121 | # Anything else (is there anything else?) 122 | echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2 123 | exit 1 124 | ;; 125 | esac 126 | 127 | # --- Finished 128 | exit 0 129 | -------------------------------------------------------------------------------- /spec/dummy/dot_git/hooks/pre-rebase.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Copyright (c) 2006, 2008 Junio C Hamano 4 | # 5 | # The "pre-rebase" hook is run just before "git rebase" starts doing 6 | # its job, and can prevent the command from running by exiting with 7 | # non-zero status. 8 | # 9 | # The hook is called with the following parameters: 10 | # 11 | # $1 -- the upstream the series was forked from. 12 | # $2 -- the branch being rebased (or empty when rebasing the current branch). 13 | # 14 | # This sample shows how to prevent topic branches that are already 15 | # merged to 'next' branch from getting rebased, because allowing it 16 | # would result in rebasing already published history. 17 | 18 | publish=next 19 | basebranch="$1" 20 | if test "$#" = 2 21 | then 22 | topic="refs/heads/$2" 23 | else 24 | topic=`git symbolic-ref HEAD` || 25 | exit 0 ;# we do not interrupt rebasing detached HEAD 26 | fi 27 | 28 | case "$topic" in 29 | refs/heads/??/*) 30 | ;; 31 | *) 32 | exit 0 ;# we do not interrupt others. 33 | ;; 34 | esac 35 | 36 | # Now we are dealing with a topic branch being rebased 37 | # on top of master. Is it OK to rebase it? 38 | 39 | # Does the topic really exist? 40 | git show-ref -q "$topic" || { 41 | echo >&2 "No such branch $topic" 42 | exit 1 43 | } 44 | 45 | # Is topic fully merged to master? 46 | not_in_master=`git rev-list --pretty=oneline ^master "$topic"` 47 | if test -z "$not_in_master" 48 | then 49 | echo >&2 "$topic is fully merged to master; better remove it." 50 | exit 1 ;# we could allow it, but there is no point. 51 | fi 52 | 53 | # Is topic ever merged to next? If so you should not be rebasing it. 54 | only_next_1=`git rev-list ^master "^$topic" ${publish} | sort` 55 | only_next_2=`git rev-list ^master ${publish} | sort` 56 | if test "$only_next_1" = "$only_next_2" 57 | then 58 | not_in_topic=`git rev-list "^$topic" master` 59 | if test -z "$not_in_topic" 60 | then 61 | echo >&2 "$topic is already up-to-date with master" 62 | exit 1 ;# we could allow it, but there is no point. 63 | else 64 | exit 0 65 | fi 66 | else 67 | not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"` 68 | /usr/bin/perl -e ' 69 | my $topic = $ARGV[0]; 70 | my $msg = "* $topic has commits already merged to public branch:\n"; 71 | my (%not_in_next) = map { 72 | /^([0-9a-f]+) /; 73 | ($1 => 1); 74 | } split(/\n/, $ARGV[1]); 75 | for my $elem (map { 76 | /^([0-9a-f]+) (.*)$/; 77 | [$1 => $2]; 78 | } split(/\n/, $ARGV[2])) { 79 | if (!exists $not_in_next{$elem->[0]}) { 80 | if ($msg) { 81 | print STDERR $msg; 82 | undef $msg; 83 | } 84 | print STDERR " $elem->[1]\n"; 85 | } 86 | } 87 | ' "$topic" "$not_in_next" "$not_in_master" 88 | exit 1 89 | fi 90 | 91 | exit 0 92 | 93 | ################################################################ 94 | 95 | This sample hook safeguards topic branches that have been 96 | published from being rewound. 97 | 98 | The workflow assumed here is: 99 | 100 | * Once a topic branch forks from "master", "master" is never 101 | merged into it again (either directly or indirectly). 102 | 103 | * Once a topic branch is fully cooked and merged into "master", 104 | it is deleted. If you need to build on top of it to correct 105 | earlier mistakes, a new topic branch is created by forking at 106 | the tip of the "master". This is not strictly necessary, but 107 | it makes it easier to keep your history simple. 108 | 109 | * Whenever you need to test or publish your changes to topic 110 | branches, merge them into "next" branch. 111 | 112 | The script, being an example, hardcodes the publish branch name 113 | to be "next", but it is trivial to make it configurable via 114 | $GIT_DIR/config mechanism. 115 | 116 | With this workflow, you would want to know: 117 | 118 | (1) ... if a topic branch has ever been merged to "next". Young 119 | topic branches can have stupid mistakes you would rather 120 | clean up before publishing, and things that have not been 121 | merged into other branches can be easily rebased without 122 | affecting other people. But once it is published, you would 123 | not want to rewind it. 124 | 125 | (2) ... if a topic branch has been fully merged to "master". 126 | Then you can delete it. More importantly, you should not 127 | build on top of it -- other people may already want to 128 | change things related to the topic as patches against your 129 | "master", so if you need further changes, it is better to 130 | fork the topic (perhaps with the same name) afresh from the 131 | tip of "master". 132 | 133 | Let's look at this example: 134 | 135 | o---o---o---o---o---o---o---o---o---o "next" 136 | / / / / 137 | / a---a---b A / / 138 | / / / / 139 | / / c---c---c---c B / 140 | / / / \ / 141 | / / / b---b C \ / 142 | / / / / \ / 143 | ---o---o---o---o---o---o---o---o---o---o---o "master" 144 | 145 | 146 | A, B and C are topic branches. 147 | 148 | * A has one fix since it was merged up to "next". 149 | 150 | * B has finished. It has been fully merged up to "master" and "next", 151 | and is ready to be deleted. 152 | 153 | * C has not merged to "next" at all. 154 | 155 | We would want to allow C to be rebased, refuse A, and encourage 156 | B to be deleted. 157 | 158 | To compute (1): 159 | 160 | git rev-list ^master ^topic next 161 | git rev-list ^master next 162 | 163 | if these match, topic has not merged in next at all. 164 | 165 | To compute (2): 166 | 167 | git rev-list master..topic 168 | 169 | if this is empty, it is fully merged to "master". 170 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitWakaTime 2 | 3 | [![Build Status](https://travis-ci.org/rposborne/gitwakatime.svg?branch=master)](https://travis-ci.org/rposborne/gitwakatime) 4 | [![Gem Version](https://badge.fury.io/rb/gitwakatime.svg)](http://badge.fury.io/rb/gitwakatime) 5 | [![Code Climate](https://codeclimate.com/github/rposborne/gitwakatime/badges/gpa.svg)](https://codeclimate.com/github/rposborne/gitwakatime) 6 | [![Test Coverage](https://codeclimate.com/github/rposborne/gitwakatime/badges/coverage.svg)](https://codeclimate.com/github/rposborne/gitwakatime/coverage) 7 | [![Issue Count](https://codeclimate.com/github/rposborne/gitwakatime/badges/issue_count.svg)](https://codeclimate.com/github/rposborne/gitwakatime) 8 | 9 | GitWakaTime is a mash up between data obtained through "[Wakatime](https://wakatime.com)" and the data we all create using git. 10 | The principal is to capture a baseline of activity for a task and answer the age old question "How much time did I spend on this?" or "What is the minimum amount I can charge for my time". 11 | 12 | This implementation varies form [Wakatime's](https://wakatime.com/#features) commit feature as it compares time spent on each file, vs comparing the time between commits. It tends to be significantly more accurate for those who do per line commits. Read more about it [here](http://burningpony.com/2015/02/that-feature-took-how-long/) 13 | 14 | ## Installation 15 | 16 | Install the gem: 17 | 18 | $ gem install gitwakatime 19 | 20 | Run the setup command: [you will need your wakatime api key](https://wakatime.com/settings) 21 | 22 | $ gitwakatime init 23 | 24 | This creates a .gitwakatime.yml file on the user's home directory ~/.gitwakatime.yml which will contain your api keys and a ~/.gitqakatime.sqlite database to speed things up a bit. 25 | 26 | ## Usage 27 | 28 | Process the current directory for the past 7 days 29 | 30 | $ gitwakatime tally 31 | 32 | Process the current directory from a given point (this will still load all heartbeats data to prevent providing incorrect timing at the start point) 33 | 34 | $ gitwakatime tally -s 2014-02-01 35 | 36 | Process the another directory 37 | 38 | $ gitwakatime tally -f ~/some/other/repo 39 | 40 | Hard reset of the local cache database, if you are getting odd numbers 41 | 42 | $ gitwakatime reset 43 | 44 | ## Assumptions 45 | 46 | There a currently a few limitations with this model 47 | 48 | * Merges are free, (no time is attributed a merge). This is true for most merges but conflict resolution will be attributed to git parent commit of that file for that merge. 49 | 50 | ## Output 51 | Total Recorded time 1 day 9 hrs 13 mins 32 secs 52 | Total Committed Time 1 day 8 hrs 43 mins 48 secs 53 | 2015-02-04 Total 2 hrs 59 mins 38 secs 54 | b1cd1d09c 2015-02-04 00:59:06 -0500 9 mins 4 secs Adding fix and test for the query class. 55 | d8ca53770 2 mins 51 secs lib/gitwakatime/query.rb 56 | d8ca53770 6 mins 13 secs spec/query_spec.rb 57 | 8e0f0890e 2015-02-04 00:46:52 -0500 26 mins 4 secs A new implementation of the split tree file issue, I think i need a more complex git log to really validate the idea. 58 | 093f9e4d5 26 mins 4 secs lib/gitwakatime/commited_file.rb 59 | d8ca53770 2015-02-04 00:46:17 -0500 54 mins 13 secs Improving testing for UTC. Fixing various bugs related to single day comparisons. 60 | 5471c6c80 6 mins 32 secs lib/gitwakatime/query.rb 61 | 08c7f7005 5 mins 40 secs lib/gitwakatime/timer.rb 62 | 5f3ec243e 27 mins 41 secs spec/commited_file_spec.rb 63 | 95e218d72 4 mins 41 secs spec/mapper_spec.rb 64 | 4949d899a 4 mins 47 secs spec/query_spec.rb 65 | 4949d899a 1 min 49 secs spec/spec_helper.rb 66 | ea23d7dd7 3 mins 3 secs spec/timer_spec.rb 67 | 30415f0a3 2015-02-04 11:54:18 -0500 1 hr 30 mins 17 secs Cache Heartbeats locally, in sqlite. 68 | 093f9e4d5 4 mins 19 secs lib/gitwakatime.rb 69 | 25 secs lib/gitwakatime/heartbeat.rb 70 | caf409884 7 mins 45 secs lib/gitwakatime/heartbeats.rb 71 | 331723757 1 min 39 secs lib/gitwakatime/cli.rb 72 | 46 mins 2 secs lib/gitwakatime/durations.rb 73 | b1cd1d09c 23 mins 50 secs lib/gitwakatime/query.rb 74 | d8ca53770 3 mins 15 secs lib/gitwakatime/timer.rb 75 | d8ca53770 1 min 20 secs spec/commited_file_spec.rb 76 | b1cd1d09c 45 secs spec/query_spec.rb 77 | d8ca53770 23 secs spec/spec_helper.rb 78 | d8ca53770 34 secs spec/timer_spec.rb 79 | 2015-02-03 Total 7 hrs 6 mins 19 secs 80 | 5f3ec243e 2015-02-03 23:20:15 -0500 11 mins 55 secs compare times in utc. 81 | 34c8f0b99 11 mins 55 secs spec/commited_file_spec.rb 82 | 4949d899a 2015-02-03 23:20:02 -0500 24 mins 5 secs Reduce log output in testing. 83 | b07136e26 18 mins 1 sec lib/gitwakatime/log.rb 84 | 39863a249 4 mins 35 secs spec/query_spec.rb 85 | 86 | ## Contributing 87 | 88 | 1. Fork it ( https://github.com/[my-github-username]/gitwakatime/fork ) 89 | 2. Create your feature branch (`git checkout -b my-new-feature`) 90 | 3. Commit your changes (`git commit -am 'Add some feature'`) 91 | 4. Push to the branch (`git push origin my-new-feature`) 92 | 5. Create a new Pull Request 93 | --------------------------------------------------------------------------------