watch_tower 0.0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. data/.gitignore +10 -0
  2. data/.todo +33 -0
  3. data/.travis.yml +14 -0
  4. data/Gemfile +43 -0
  5. data/Guardfile +25 -0
  6. data/MIT-LICENSE +20 -0
  7. data/README.md +38 -0
  8. data/Rakefile +8 -0
  9. data/TODO +17 -0
  10. data/bin/watchtower +10 -0
  11. data/ci/adapters/jruby-mysql.yml +8 -0
  12. data/ci/adapters/jruby-postgresql.yml +6 -0
  13. data/ci/adapters/jruby-sqlite.yml +6 -0
  14. data/ci/adapters/ruby-mysql.yml +8 -0
  15. data/ci/adapters/ruby-postgresql.yml +6 -0
  16. data/ci/adapters/ruby-sqlite.yml +6 -0
  17. data/ci/travis.rb +102 -0
  18. data/lib/watch_tower.rb +60 -0
  19. data/lib/watch_tower/appscript.rb +22 -0
  20. data/lib/watch_tower/cli.rb +15 -0
  21. data/lib/watch_tower/cli/.gitkeep +0 -0
  22. data/lib/watch_tower/cli/install.rb +63 -0
  23. data/lib/watch_tower/cli/open.rb +24 -0
  24. data/lib/watch_tower/cli/start.rb +140 -0
  25. data/lib/watch_tower/config.rb +38 -0
  26. data/lib/watch_tower/core_ext.rb +4 -0
  27. data/lib/watch_tower/core_ext/.gitkeep +0 -0
  28. data/lib/watch_tower/editor.rb +17 -0
  29. data/lib/watch_tower/editor/.gitkeep +0 -0
  30. data/lib/watch_tower/editor/base_appscript.rb +34 -0
  31. data/lib/watch_tower/editor/base_ps.rb +6 -0
  32. data/lib/watch_tower/editor/textmate.rb +17 -0
  33. data/lib/watch_tower/editor/xcode.rb +22 -0
  34. data/lib/watch_tower/errors.rb +25 -0
  35. data/lib/watch_tower/eye.rb +79 -0
  36. data/lib/watch_tower/project.rb +14 -0
  37. data/lib/watch_tower/project/.gitkeep +0 -0
  38. data/lib/watch_tower/project/any_based.rb +22 -0
  39. data/lib/watch_tower/project/git_based.rb +86 -0
  40. data/lib/watch_tower/project/init.rb +38 -0
  41. data/lib/watch_tower/project/path_based.rb +144 -0
  42. data/lib/watch_tower/server.rb +62 -0
  43. data/lib/watch_tower/server/.gitkeep +0 -0
  44. data/lib/watch_tower/server/app.rb +37 -0
  45. data/lib/watch_tower/server/assets/images/WatchTower.jpg +0 -0
  46. data/lib/watch_tower/server/assets/images/percentage.png +0 -0
  47. data/lib/watch_tower/server/assets/javascripts/application.js +3 -0
  48. data/lib/watch_tower/server/assets/javascripts/percentage.coffee +8 -0
  49. data/lib/watch_tower/server/assets/stylesheets/application.css +7 -0
  50. data/lib/watch_tower/server/assets/stylesheets/global.sass +71 -0
  51. data/lib/watch_tower/server/assets/stylesheets/project.sass +59 -0
  52. data/lib/watch_tower/server/configurations.rb +10 -0
  53. data/lib/watch_tower/server/configurations/asset.rb +43 -0
  54. data/lib/watch_tower/server/database.rb +105 -0
  55. data/lib/watch_tower/server/db/migrate/001_create_projects.rb +15 -0
  56. data/lib/watch_tower/server/db/migrate/002_create_files.rb +16 -0
  57. data/lib/watch_tower/server/db/migrate/003_create_time_entries.rb +12 -0
  58. data/lib/watch_tower/server/db/migrate/004_create_durations.rb +14 -0
  59. data/lib/watch_tower/server/db/migrate/005_add_hash_to_time_entries.rb +6 -0
  60. data/lib/watch_tower/server/db/migrate/006_add_hash_to_files.rb +6 -0
  61. data/lib/watch_tower/server/decorator.rb +21 -0
  62. data/lib/watch_tower/server/decorator/application_decorator.rb +91 -0
  63. data/lib/watch_tower/server/decorator/file_decorator.rb +38 -0
  64. data/lib/watch_tower/server/decorator/project_decorator.rb +51 -0
  65. data/lib/watch_tower/server/helpers.rb +13 -0
  66. data/lib/watch_tower/server/helpers/asset.rb +29 -0
  67. data/lib/watch_tower/server/helpers/improved_partials.rb +41 -0
  68. data/lib/watch_tower/server/models/duration.rb +11 -0
  69. data/lib/watch_tower/server/models/file.rb +31 -0
  70. data/lib/watch_tower/server/models/project.rb +17 -0
  71. data/lib/watch_tower/server/models/time_entry.rb +64 -0
  72. data/lib/watch_tower/server/public/assets/WatchTower-4d6de11e1bd34165ad91ac46fb711bf3.jpg +0 -0
  73. data/lib/watch_tower/server/public/assets/application-7829b53b5ece1a16d22dc3d00f329023.css +107 -0
  74. data/lib/watch_tower/server/public/assets/application-e0e6b7731aade460f680331e65cf0682.js +9359 -0
  75. data/lib/watch_tower/server/public/assets/percentage-d8589e21a5fc85d32a445f531ff8ab95.png +0 -0
  76. data/lib/watch_tower/server/vendor/assets/javascripts/jquery-ui.js +11729 -0
  77. data/lib/watch_tower/server/vendor/assets/javascripts/jquery.js +8981 -0
  78. data/lib/watch_tower/server/vendor/assets/javascripts/jquery_ujs.js +363 -0
  79. data/lib/watch_tower/server/views/.gitkeep +0 -0
  80. data/lib/watch_tower/server/views/_file.haml +9 -0
  81. data/lib/watch_tower/server/views/_project.haml +13 -0
  82. data/lib/watch_tower/server/views/index.haml +7 -0
  83. data/lib/watch_tower/server/views/layout.haml +32 -0
  84. data/lib/watch_tower/server/views/project.haml +12 -0
  85. data/lib/watch_tower/templates/config.yml +146 -0
  86. data/lib/watch_tower/templates/watchtower.plist +23 -0
  87. data/lib/watch_tower/version.rb +8 -0
  88. data/spec/factories.rb +45 -0
  89. data/spec/spec_helper.rb +26 -0
  90. data/spec/support/active_record.rb +44 -0
  91. data/spec/support/factory_girl.rb +6 -0
  92. data/spec/support/launchy.rb +3 -0
  93. data/spec/support/sinatra.rb +10 -0
  94. data/spec/support/timecop.rb +7 -0
  95. data/spec/watch_tower/appscript_spec.rb +6 -0
  96. data/spec/watch_tower/cli/install_spec.rb +16 -0
  97. data/spec/watch_tower/cli/open_spec.rb +14 -0
  98. data/spec/watch_tower/cli/start_spec.rb +17 -0
  99. data/spec/watch_tower/cli_spec.rb +15 -0
  100. data/spec/watch_tower/config_spec.rb +25 -0
  101. data/spec/watch_tower/editor/textmate_spec.rb +43 -0
  102. data/spec/watch_tower/editor/xcode_spec.rb +43 -0
  103. data/spec/watch_tower/editor_spec.rb +19 -0
  104. data/spec/watch_tower/eye_spec.rb +130 -0
  105. data/spec/watch_tower/project/git_based_spec.rb +131 -0
  106. data/spec/watch_tower/project/path_based_spec.rb +111 -0
  107. data/spec/watch_tower/project_spec.rb +82 -0
  108. data/spec/watch_tower/server/app_spec.rb +186 -0
  109. data/spec/watch_tower/server/decorator/project_decorator_spec.rb +60 -0
  110. data/spec/watch_tower/server/models/file_spec.rb +284 -0
  111. data/spec/watch_tower/server/models/project_spec.rb +165 -0
  112. data/spec/watch_tower/server/models/time_entry_spec.rb +37 -0
  113. data/spec/watch_tower/server_spec.rb +4 -0
  114. data/watch_tower.gemspec +80 -0
  115. metadata +450 -0
@@ -0,0 +1,79 @@
1
+ require 'digest/sha1'
2
+
3
+ module WatchTower
4
+ module Eye
5
+ extend self
6
+
7
+ # Start the watch loop
8
+ #
9
+ # @param [Hash] options
10
+ # @raise [EyeError]
11
+ def start(options = {})
12
+ LOG.debug("#{__FILE__}:#{__LINE__}: The Eye loop has just started")
13
+ loop do
14
+ # Try getting the mtime of the document opened by each editor in the
15
+ # editors list.
16
+ Editor.editors.each do |editor|
17
+ # Create an instance of the editor
18
+ # TODO: Should be used as a class instead
19
+ editor = editor.new
20
+ # Check if the editor is running
21
+ if editor.is_running?
22
+ LOG.debug("#{__FILE__}:#{__LINE__}: #{editor.to_s} is running")
23
+ # Get the currently being edited file from the editor
24
+ files_paths = editor.current_paths
25
+ files_paths.each do |file_path|
26
+ begin
27
+ # Get the file_hash of the file
28
+ file_hash = Digest::SHA1.file(file_path).hexdigest
29
+ # Create a project from the file_path
30
+ project = Project.new_from_path(file_path)
31
+ rescue PathNotUnderCodePath
32
+ LOG.debug("#{__FILE__}:#{__LINE__ - 2}: The file '#{file_path}' is not located under '#{Config[:code_path]}', it has been ignored")
33
+ next
34
+ rescue FileNotFound
35
+ LOG.debug "#{__FILE__}:#{__LINE__ - 5}: The file '#{file_path}' does not exist, it has been ignored"
36
+ next
37
+ end
38
+
39
+ begin
40
+ # Create (or fetch) a project
41
+ project_model = Server::Project.find_or_create_by_name_and_path(project.name, project.path)
42
+
43
+ # Create (or fetch) a file
44
+ file_model = project_model.files.find_or_create_by_path(file_path)
45
+ begin
46
+ # Create a time entry
47
+ file_model.time_entries.create!(mtime: File.stat(file_path).mtime, file_hash: file_hash)
48
+ rescue ActiveRecord::RecordInvalid => e
49
+ # This should happen if the mtime is already present
50
+ end
51
+ rescue ActiveRecord::RecordInvalid => e
52
+ # This should not happen
53
+ LOG.fatal("#{__FILE__}:#{__LINE__}: #{e}")
54
+ $close_eye = true
55
+ end
56
+ end
57
+ else
58
+ LOG.debug("#{__FILE__}:#{__LINE__}: #{editor.to_s} is not running")
59
+ end
60
+ end
61
+
62
+ # If $stop global is set, please stop, otherwise sleep for 30 seconds.
63
+ if $close_eye
64
+ LOG.debug("#{__FILE__}:#{__LINE__}: Closing eye has been requested, end the loop")
65
+ break
66
+ else
67
+ sleep 10
68
+ end
69
+ end
70
+ end
71
+
72
+ # Start the Eye, a method invoked from the Watch Tower command line interface
73
+ #
74
+ # @param [Hash] options
75
+ def start!(options = {})
76
+ start(options)
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,14 @@
1
+ require 'watch_tower/project/any_based'
2
+ require 'watch_tower/project/init'
3
+
4
+ module WatchTower
5
+ class Project
6
+ extend ::ActiveSupport::Autoload
7
+
8
+ autoload :GitBased
9
+ autoload :PathBased
10
+
11
+ include AnyBased
12
+ include Init
13
+ end
14
+ end
File without changes
@@ -0,0 +1,22 @@
1
+ module WatchTower
2
+ class Project
3
+ module AnyBased
4
+
5
+ def self.included(base)
6
+ base.send :include, InstanceMethods
7
+ base.extend ClassMethods
8
+ end
9
+
10
+ module InstanceMethods
11
+ attr_reader :name, :path
12
+ end
13
+
14
+ module ClassMethods
15
+ protected
16
+ def expand_path(path)
17
+ File.expand_path(path)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,86 @@
1
+ require 'git'
2
+
3
+ module WatchTower
4
+ class Project
5
+ module GitBased
6
+ extend self
7
+ include AnyBased
8
+
9
+ # Cache for working_directory by path
10
+ # The key is the path to a file, the value is the working directory of
11
+ # this path
12
+ @@working_cache = Hash.new
13
+
14
+ # Cache for project_name by path
15
+ # The key is the path to a file, the value is the project's name
16
+ @@project_name_cache = Hash.new
17
+
18
+ # Cache for project git path
19
+ # The key is the path to a file, the value is the project's parts
20
+ @@project_git_folder_path = Hash.new
21
+
22
+ # Check if the path is under Git
23
+ #
24
+ # @param path The path we should check if it's under Git control
25
+ # @param [Hash] options A hash of options
26
+ # @return boolean
27
+ def active_for_path?(path, options = {})
28
+ path = expand_path path
29
+ project_git_folder_path(path).present?
30
+ end
31
+
32
+ # Return the working directory (the project's path if you will) from a path
33
+ # to any file inside the project
34
+ #
35
+ # @param path The path to look the project path from
36
+ # @param [Hash] options A hash of options
37
+ # @return [String] the project's folder
38
+ def working_directory(path, options = {})
39
+ path = expand_path path
40
+ return @@working_cache[path] if @@working_cache.key?(path)
41
+
42
+ @@working_cache[path] = File.dirname(project_git_folder_path(path))
43
+ @@working_cache[path]
44
+ end
45
+
46
+ # Return the project's name from a path to any file inside the project
47
+ #
48
+ # @param path The path to look the project path from
49
+ # @param [Hash] options A hash of options
50
+ # @return [String] the project's name
51
+ def project_name(path, options = {})
52
+ path = expand_path path
53
+ return @@project_name_cache[path] if @@project_name_cache.key?(path)
54
+
55
+ @@project_name_cache[path] = File.basename working_directory(path, options)
56
+ @@project_name_cache[path]
57
+ end
58
+
59
+ def head(path)
60
+ log(path)
61
+ end
62
+
63
+ def log(path)
64
+ g = ::Git.open path
65
+ g.log.first
66
+ end
67
+
68
+ protected
69
+ def project_git_folder_path(path)
70
+ return @@project_git_folder_path[path] if @@project_git_folder_path.key?(path)
71
+
72
+ # Define the start
73
+ n = 0
74
+ # Define the maximum search folder
75
+ max_n = path.split('/').size
76
+
77
+ until File.exists?(File.expand_path File.join(path, (%w{..} * n).flatten, '.git')) || n > max_n
78
+ n = n + 1
79
+ end
80
+
81
+ @@project_git_folder_path[path] = n <= max_n ? File.expand_path(File.join(path, (%w{..} * n).flatten, '.git')) : nil
82
+ @@project_git_folder_path[path]
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,38 @@
1
+ module WatchTower
2
+ class Project
3
+ module Init
4
+ def self.included(base)
5
+ base.extend ClassMethods
6
+ base.send :include, InstanceMethods
7
+ end
8
+
9
+ module ClassMethods
10
+ # Create a new project from a path (to a file or a folder)
11
+ #
12
+ # @param [String] path, the path to the file
13
+ # @return [Project] a new initialized project
14
+ def new_from_path(path)
15
+ raise FileNotFound unless path && File.exists?(path)
16
+ LOG.debug("#{__FILE__}:#{__LINE__}: Creating a project from #{path}")
17
+ if GitBased.active_for_path?(path)
18
+ Project.new GitBased.project_name(path), GitBased.working_directory(path)
19
+ else
20
+ Project.new PathBased.project_name(path), PathBased.working_directory(path)
21
+ end
22
+ end
23
+ end
24
+
25
+ module InstanceMethods
26
+ # Initialize a project using a name and a path
27
+ #
28
+ # @param [String] name: the name of the project
29
+ # @param [String] path: The path of the project
30
+ def initialize(name, path)
31
+ LOG.debug("#{__FILE__}:#{__LINE__}: Created project #{name} located at #{path}")
32
+ @name = name
33
+ @path = path
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,144 @@
1
+ module WatchTower
2
+ class Project
3
+ # The module contains Path specific project methods, methods like name, path
4
+ # The module can be included into another module/class or used on it's own,
5
+ # it does extend itself so any methods defined here is available to both class
6
+ # and instance level
7
+ module PathBased
8
+ include AnyBased
9
+ extend self
10
+
11
+ # Cache for working_directory by path
12
+ # The key is the path to a file, the value is the working directory of
13
+ # this path
14
+ @@working_cache = Hash.new
15
+
16
+ # Cache for project_name by path
17
+ # The key is the path to a file, the value is the project's name
18
+ @@project_name_cache = Hash.new
19
+
20
+ # Cache for project path parts
21
+ # The key is the path to a file, the value is the project's parts
22
+ @@project_path_part_cache = Hash.new
23
+
24
+ # Return the working directory (the project's path if you will) from a path
25
+ # to any file inside the project
26
+ #
27
+ # @param [String] path The path to look the project path from
28
+ # @param [Hash] options A hash of options
29
+ # @return [String] the project's folder
30
+ def working_directory(path, options = {})
31
+ return @@working_cache[path] if @@working_cache.key?(path)
32
+
33
+
34
+ @@working_cache[path] = project_path_from_nested_path(code_path(options),
35
+ path, nested_project_layers(options))
36
+ @@working_cache[path]
37
+ end
38
+
39
+ # Return the project's name from a path to any file inside the project
40
+ #
41
+ # @param path The path to look the project path from
42
+ # @param [Hash] options A hash of options
43
+ # @return [String] the project's name
44
+ def project_name(path, options = {})
45
+ return @@project_name_cache[path] if @@project_name_cache.key?(path)
46
+
47
+ @@project_name_cache[path] = project_name_from_nested_path(code_path(options),
48
+ path, nested_project_layers(options))
49
+ @@project_name_cache[path]
50
+ end
51
+
52
+ protected
53
+
54
+ # Get the code path from the options, if not found use the one from the
55
+ # configurations
56
+ #
57
+ # @param [Hash] options
58
+ # @return [String] The Code path
59
+ def code_path(options = {})
60
+ options[:code_path] || Config[:code_path]
61
+ end
62
+
63
+ # Get the nested_project_layers from the options, if not found use the
64
+ # one from the configurations
65
+ #
66
+ # @param [Hash] options
67
+ # @return [String] The nested_project_layers
68
+ def nested_project_layers(options = {})
69
+ options[:nested_project_layers] || Config[:nested_project_layers]
70
+ end
71
+
72
+ # Taken from timetap
73
+ # https://github.com/elia/timetap/blob/master/lib/time_tap/project.rb#L40
74
+ #
75
+ # Find out the path parts of the project that's currently being worked on,
76
+ # under the code path, it uses the param nested_project_layers to determine
77
+ # the project name from the entire expanded path to any file under the
78
+ # project
79
+ #
80
+ # nested project layers works "how many folders inside your code folder
81
+ # do you keep projects.
82
+ #
83
+ # For example, if your directory structure looks like:
84
+ # ~/Code/
85
+ # Clients/
86
+ # AcmeCorp/
87
+ # website/
88
+ # intranet
89
+ # BetaCorp/
90
+ # skunkworks/
91
+ # OpenSource/
92
+ # project_one/
93
+ # timetap/
94
+ #
95
+ # A nested_project_layers setting of 2 would mean we track "AcmeCorp", "BetaCorp", and everything
96
+ # under OpenSource, as their own projects
97
+ #
98
+ # @param code The path you store all the projects under
99
+ # @param path The path to look the project name from
100
+ # @param nested_project_layers How many folders, defaults to 2
101
+ # @return [Array] The project path's parts
102
+ # @raise [WatchTower::PathNotUnderCodePath] if the path is not nested under code
103
+ def project_path_part(code, path, nested_project_layers = 2)
104
+ return @@project_path_part_cache[path] if @@project_path_part_cache.key?(path)
105
+
106
+ # Expand pathes
107
+ code = expand_path code
108
+ path = expand_path path
109
+
110
+ regex_suffix = "([^/]+)"
111
+ regex_suffix = [regex_suffix] * nested_project_layers
112
+ regex_suffix = regex_suffix.join("/")
113
+
114
+ path.scan(%r{(#{code})/#{regex_suffix}}).flatten.collect(&:chomp).
115
+ tap { |r| raise PathNotUnderCodePath unless r.any? }.
116
+ tap { |ppp| @@project_path_part_cache[path] = ppp }
117
+ end
118
+
119
+ # Find out the project's name
120
+ # See #project_path_part
121
+ #
122
+ # @param code The path you store all the projects under
123
+ # @param path The path to look the project name from
124
+ # @param nested_project_layers How many folders, defaults to 2
125
+ # @return [String] The project's name
126
+ # @raise [WatchTower::PathNotUnderCodePath] if the path is not nested under code
127
+ def project_name_from_nested_path(code, path, nested_project_layers = 2)
128
+ project_path_part(code, path, nested_project_layers)[nested_project_layers]
129
+ end
130
+
131
+ # Find out the project's path
132
+ # See #project_path_part
133
+ #
134
+ # @param code The path you store all the projects under
135
+ # @param path The path to look the project name from
136
+ # @param nested_project_layers How many folders, defaults to 2
137
+ # @return [String] The project's path
138
+ # @raise [WatchTower::PathNotUnderCodePath] if the path is not nested under code
139
+ def project_path_from_nested_path(code, path, nested_project_layers = 2)
140
+ project_path_part(code, path, nested_project_layers)[0..nested_project_layers].join('/')
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,62 @@
1
+ module WatchTower
2
+ module Server
3
+ extend ::ActiveSupport::Autoload
4
+
5
+ autoload :Database
6
+ autoload :Duration, ::File.join(MODELS_PATH, 'duration.rb')
7
+ autoload :Project, ::File.join(MODELS_PATH, 'project.rb')
8
+ autoload :File, ::File.join(MODELS_PATH, 'file.rb')
9
+ autoload :TimeEntry, ::File.join(MODELS_PATH, 'time_entry.rb')
10
+ autoload :Helpers
11
+ autoload :Configurations
12
+ autoload :Decorator
13
+ autoload :App
14
+
15
+ # Start the server
16
+ # This method starts the database and then starts the server
17
+ #
18
+ # @param [Hash] options
19
+ def self.start(options = {})
20
+ # Start the Database
21
+ Database.start!(options)
22
+
23
+ # Start the Sinatra application
24
+ start_web_server(options)
25
+ end
26
+
27
+ # Start the Server, a method invoked from the Watch Tower command line interface
28
+ #
29
+ # @param [Hash] options
30
+ def self.start!(options = {})
31
+ start(options)
32
+ end
33
+
34
+ protected
35
+ # Start the web_server
36
+ # This method starts the web server (The Sinatra app)
37
+ #
38
+ # @param [Hash] options
39
+ def self.start_web_server(options = {})
40
+ LOG.debug("#{__FILE__}:#{__LINE__}: Starting the Sinatra App")
41
+
42
+ # Abort execution if the Thread raised an error.
43
+ Thread.abort_on_exception = true
44
+
45
+ WatchTower.threads[:web_server] = Thread.new do
46
+ LOG.debug("#{__FILE__}:#{__LINE__}: Starting a new Thread for the web server.")
47
+
48
+ begin
49
+ LOG.debug("#{__FILE__}:#{__LINE__}: Starting the web server in the new Thread.")
50
+
51
+ # Start the server
52
+ App.run!(options)
53
+
54
+ LOG.debug("#{__FILE__}:#{__LINE__}: The server has stopped.")
55
+ rescue Exception => e
56
+ LOG.fatal "#{__FILE__}:#{__LINE__ - 4}: #{e}"
57
+ raise e
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end