watch_tower 0.0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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