hubeye 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 by Luke Gruber
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included
12
+ in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
18
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
19
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
20
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,74 @@
1
+ #Hubeye
2
+
3
+ Hubeye is composed of a client and server. Once the server is run,
4
+ you can connect to it via the client. Once connected, you'll be
5
+ prompted by a '>'. Type the name of a Github repository.
6
+
7
+ Example: (what the user enters is preceded by the prompt)
8
+
9
+ >hubeye
10
+ commit 77b82b54044c16751228
11
+ tree 8ce18af1461b5c741003
12
+ parent ea63fe317fe58dff1c95
13
+ log tracking info for repos on client quit => luke-gru
14
+
15
+ What you see is the latest commit reference, tree reference and parent
16
+ commit reference on Github for that repository. Note that the user did
17
+ not type a username. This is because the user defined a username in his
18
+ ~/.hubeyerc file.
19
+
20
+ ###~/.hubeyerc
21
+
22
+ username = luke-gru
23
+
24
+ This allows the user to type the repository name only, and to receive
25
+ information regarding that user's repository. The username must be a
26
+ valid Github username.
27
+
28
+ ###Keeping track of repositories
29
+
30
+ Hubeye doesn't actually track any repositories unless you disconnect
31
+ from the server and leave the server running. This can be done by:
32
+
33
+ >quit
34
+ Bye!
35
+
36
+ If Hubeye has any repos to watch, it will watch Github for changes.
37
+ It can keep track of as many repos as you want; just keep typing
38
+ them in. If Hubeye finds a change in a repo, it will tell you that the
39
+ repo has changed in the terminal where the server is running. Also, next
40
+ time you connect to the server via the client, it will remind you.
41
+ Hubeye does not remove a repo from the watch list unless explicitly
42
+ told to do so.
43
+
44
+ >rm luke-gru/hubeye
45
+
46
+ this removes luke-gru's hubeye repo from the watch list. You can also
47
+ add another user's repo like this:
48
+
49
+ >rails/rails
50
+
51
+ This adds https://github.com/rails/rails to the watch list.
52
+
53
+ ###Shutting down
54
+
55
+ >shutdown
56
+
57
+ Next time you start up the server, the watch list is empty.
58
+ In order to have a default watch list:
59
+
60
+ ~/.hubeyerc
61
+
62
+ default = rails/rails | dchelimsky/rspec
63
+
64
+ These will be watched automatically when starting up the server
65
+
66
+ ###All ~/.hubeyerc configurations
67
+
68
+ username = luke-gru
69
+ default = username/reponame | username2/reponame2 | myreponame
70
+ oncearound = 90
71
+
72
+ oncearound: number of seconds before completing a check of every repo in
73
+ the watch list for changes
74
+
data/Rakefile ADDED
@@ -0,0 +1,34 @@
1
+ task :install => :chmod do
2
+ puts "Done"
3
+ end
4
+
5
+ task :chmod => :makelog do
6
+ binfile = File.join(File.dirname(__FILE__), "/bin/hubeye")
7
+ chmod 0777, binfile unless File.executable?(binfile)
8
+ end
9
+
10
+ task :makelog => :config_file do
11
+ hublog_dir = ENV['HOME'] + "/hublog"
12
+ mkdir(hublog_dir) unless File.exists?(hublog_dir)
13
+
14
+ hooks_dir = hublog_dir + "/hooks"
15
+ mkdir(hooks_dir) unless File.exists?(hooks_dir)
16
+
17
+ repos_dir = hublog_dir + "/repos"
18
+ mkdir(repos_dir) unless File.exists?(repos_dir)
19
+
20
+ hublog_file = File.join(ENV['HOME'], "/hublog/hublog")
21
+ touch hublog_file unless File.exists?(hublog_file)
22
+ end
23
+
24
+ task :config_file do
25
+ config_file = File.join(ENV['HOME'], ".hubeyerc")
26
+ touch config_file unless File.exists? config_file
27
+ end
28
+
29
+ task :config_file => :message
30
+
31
+ task :message do
32
+ puts "Installing Hubeye..."
33
+ end
34
+
data/VERSION.rb ADDED
@@ -0,0 +1,5 @@
1
+ #/* vim: set ft=ruby :*/
2
+ class Hubeye
3
+ VERSION = [0,0,1]
4
+ end
5
+
data/bin/hubeye ADDED
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # require environment and VERSION files
4
+ require File.join(File.expand_path(File.dirname(__FILE__) + "/../"), "lib/environment")
5
+ require File.expand_path(File.dirname(__FILE__) + "/../VERSION")
6
+
7
+ # standard lib
8
+ require 'optparse'
9
+ require 'ostruct'
10
+
11
+ # vendor
12
+ unless RUBY_VERSION >= '1.9'
13
+ begin
14
+ require 'daemons'
15
+ rescue
16
+ require 'rubygems'
17
+ retry
18
+ else
19
+ RuntimeError.new "The daemons gem is needed to run hubeye as a daemon " +
20
+ "on Ruby < 1.9"
21
+ end
22
+ end
23
+
24
+ class Options
25
+ def self.parse(args)
26
+
27
+ # defaults
28
+ options = OpenStruct.new
29
+ options.server_wanted = true
30
+ options.client_wanted = false
31
+ options.server_daemonized = true
32
+ options.port = 2000
33
+ options.host = 'localhost'
34
+
35
+ opts_saved = OptionParser.new do |opts|
36
+ opts.banner = "Usage: hubeye [options]"
37
+ opts.separator ""
38
+ opts.separator "Specific options:"
39
+ opts.separator "Note: The default port (for server and client) is 2000"
40
+
41
+ opts.on("-s", "--server", "Start the server (default: daemonized process)") do
42
+ options.client_wanted = false
43
+ options.server_wanted = true
44
+ end
45
+
46
+ opts.on("-t", "--top", "Run server as non-daemonized (output to term)") do
47
+ options.server_daemonized = false
48
+ end
49
+
50
+ opts.on("-o", "--host HOST", "Host that the client connects to. Default: 'localhost'") do |h|
51
+ options.host = h
52
+ end
53
+
54
+ opts.on("-p", "--port PORT", "Port that the server runs on / client connects to") do |p|
55
+ options.port = p.to_i
56
+ end
57
+
58
+ opts.on("-c", "--client", "Start hubeye client to interact with server.") do
59
+ options.server_wanted = false
60
+ options.client_wanted = true
61
+ end
62
+
63
+ opts.on_tail("-v", "--version", "Show hubeye version") do
64
+ puts Hubeye::VERSION.join('.')
65
+ exit
66
+ end
67
+
68
+ opts.on_tail("-h", "--help", "Show this message") do
69
+ puts opts
70
+ exit
71
+ end
72
+ end
73
+
74
+ opts_saved.parse!(args)
75
+ options
76
+ end # end of Options::parse
77
+
78
+ end # end of class
79
+
80
+
81
+ class Hubeye
82
+ class << self
83
+
84
+ def start
85
+ options = Options.parse(ARGV)
86
+ host = options.host
87
+ port = options.port
88
+ daemonized = options.server_daemonized
89
+ if options.server_wanted
90
+ unless port_open?(port)
91
+ puts "Other instances are running"
92
+ exit 1
93
+ end
94
+ require File.join(Environment::LIBDIR, '/server/hubeye_server.rb')
95
+ options.server_daemonized ? start_server(port, :daemon => true) : start_server(port, :daemon => false)
96
+ else
97
+ require File.join(Environment::LIBDIR, '/client/hubeye_client.rb')
98
+ start_client(host, port)
99
+ end
100
+ end
101
+
102
+
103
+ def port_open?(port)
104
+ # this is the solution until multi-threading is implemented
105
+ # on the TCP server
106
+ listening_tcps = `netstat -l --tcp --numeric`
107
+ if /#{port}/ =~ listening_tcps
108
+ return false
109
+ end
110
+ true
111
+ end
112
+
113
+
114
+ def start_server(port, options={})
115
+ server = HubeyeServer.new(true) # debug: true
116
+ if options[:daemon]
117
+ Process.daemon(true) # don't change dir to '/'
118
+ end
119
+ server.start(port, options)
120
+ end
121
+
122
+ # client connection defaults to localhost:2000
123
+ def start_client(host, port)
124
+ client = HubeyeClient.new
125
+ client.start(host, port)
126
+ end
127
+
128
+ end
129
+ end
130
+
131
+ Hubeye.start
132
+
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env ruby
2
+ require 'socket'
3
+
4
+ class HubeyeClient
5
+
6
+ def start(host, port)
7
+ connect(host, port)
8
+ read_welcome()
9
+ interact()
10
+ end
11
+
12
+ def _begin
13
+ begin
14
+ yield
15
+ rescue
16
+ puts $! # Display the exception to the user
17
+ end
18
+ end
19
+
20
+ def connect(host, port)
21
+ _begin do
22
+ # Give the user some feedback while connecting.
23
+ STDOUT.print "Connecting..." # Say what we're doing
24
+ STDOUT.flush # Make it visible right away
25
+ @s = TCPSocket.open(host, port) # Connect
26
+ STDOUT.puts "Done" if @s
27
+
28
+ # Now display information about the connection.
29
+ local, peer = @s.addr, @s.peeraddr
30
+
31
+ STDOUT.print "Connected to #{peer[2]}:#{peer[1]}"
32
+ STDOUT.puts " using local port #{local[1]}"
33
+ end
34
+ end
35
+
36
+ def read_welcome
37
+ # Wait just a bit, to see if the server sends any initial message.
38
+ begin
39
+ sleep(0.5) # Wait half a second
40
+ msg = @s.readpartial(4096) # Read whatever is ready
41
+ STDOUT.puts msg.chop # And display it
42
+ # If nothing was ready to read, just ignore the exception.
43
+ rescue SystemCallError
44
+ rescue NoMethodError
45
+ STDOUT.puts "The server's not running!"
46
+ end
47
+ end
48
+
49
+ # Now begin a loop of client/server interaction.
50
+ def interact
51
+ while @s
52
+ loop do
53
+ STDOUT.print '> ' # Display prompt for local input
54
+ STDOUT.flush # Make sure the prompt is visible
55
+ local = STDIN.gets # Read line from the console
56
+ #break if !local # Quit if no input from console
57
+ if local.match(/^\.$/) #pwd
58
+ @s.puts(local.gsub(/\A\.\Z/, "pwd" + Dir.pwd.split('/').last)) # Send the line to the server, daemons gem strips some special chars (/, :)
59
+ else
60
+ @s.puts(local.gsub(/\//, 'diiv'))
61
+ end
62
+ @s.flush # Force it out
63
+ # Read the server's response and print out.
64
+ # The server may send more than one line, so use readpartial
65
+ # to read whatever it sends (as long as it all arrives in one chunk).
66
+
67
+ sleep 0.5
68
+ begin
69
+ response = @s.readpartial(4096)
70
+ rescue EOFError
71
+ response = "Bye!"
72
+ end
73
+
74
+ if response.chop.strip == "Bye!"
75
+ puts(response.chop)
76
+ @s.close
77
+ exit
78
+ elsif response.chop.strip.match(/shutting/i)
79
+ @s.close
80
+ exit
81
+ else
82
+ puts(response.chop) # Display response to user
83
+ next
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ end #end of class
90
+
@@ -0,0 +1,10 @@
1
+ module Environment
2
+
3
+ ROOTDIR = File.join(File.dirname(__FILE__), "..")
4
+
5
+ LIBDIR = File.join(ROOTDIR, "lib")
6
+ BINDIR = File.join(ROOTDIR, "bin")
7
+
8
+ $:.unshift(LIBDIR) unless $:.include? LIBDIR
9
+
10
+ end
@@ -0,0 +1,30 @@
1
+ module Hooks
2
+ class Command
3
+
4
+ class NoHookError < ArgumentError; end
5
+
6
+ #options include the directory to execute the command
7
+ #(that's it for now, will add more functionality later)
8
+ def self.execute(commands=[], options={})
9
+ opts = {:directory => nil, :repo => nil}.merge(options)
10
+ dir = opts[:directory]
11
+ repo = opts[:repo]
12
+ begin
13
+
14
+ commands.each do |cmd|
15
+ if dir
16
+ Dir.chdir(File.expand_path(dir)) do
17
+ ::Kernel.system cmd
18
+ end
19
+ else
20
+ ::Kernel.system cmd
21
+ end
22
+ end
23
+
24
+ rescue ArgumentError
25
+ raise NoHookError.new "There aren't any hook commands for the repository #{repo}"
26
+ end
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,25 @@
1
+ module Hooks
2
+ module Git
3
+
4
+ module Default
5
+
6
+ def self.fetch(local_reponame, remotename, branchname)
7
+ #first, start a process to cd to the local repo
8
+ #then fetch the remote
9
+ Dir.chdir(File.expand_path(local_reponame)) do
10
+ ::Kernel.system("git fetch #{remotename} #{branchname}")
11
+ end
12
+ end
13
+
14
+ def self.pull(local_reponame, remotename, branchname)
15
+ #first, start a process to cd to the local repo
16
+ #then pull the remote
17
+ Dir.chdir(File.expand_path(local_reponame)) do
18
+ ::Kernel.system("git pull #{remotename} #{branchname}")
19
+ end
20
+ end
21
+
22
+ end
23
+
24
+ end
25
+ end
data/lib/log/logger.rb ADDED
@@ -0,0 +1,39 @@
1
+ class Logger
2
+
3
+ def self.log(msg)
4
+ File.open(ENV['HOME'] + "/hublog" * 2, "a") do |f|
5
+ f.puts(msg)
6
+ end
7
+ end
8
+
9
+ def self.relog(msg)
10
+ #wipe the file and start anew
11
+ File.open(ENV['HOME'] + "/hublog" * 2, "w") do |f|
12
+ f.puts(msg)
13
+ end
14
+ end
15
+
16
+ ##If {include_socket: true} is passed, then log to the client as well. If
17
+ #{include_terminal: true}, log to the terminal too (make sure that the
18
+ #process is not daemonized). Always log to the logfile.
19
+
20
+ def self.log_change(repo_name, commit_msg, committer, options={})
21
+ opts = {:include_socket => false, :include_terminal => false}.merge options
22
+ change_msg = <<-MSG
23
+ ===============================
24
+ Repository: #{repo_name.downcase.strip} has changed (#{Time.now.strftime("%m/%d/%Y at %I:%M%p")})
25
+ Commit msg: #{commit_msg}
26
+ Committer : #{committer}
27
+ ===============================
28
+ MSG
29
+ if opts[:include_socket]
30
+ socket.puts(change_msg)
31
+ end
32
+
33
+ if opts[:include_terminal]
34
+ puts change_msg
35
+ end
36
+ Logger.log(change_msg)
37
+ end
38
+
39
+ end
@@ -0,0 +1,11 @@
1
+ module Autotest::GnomeNotify
2
+
3
+ EXPIRATION_IN_SECONDS = 2
4
+ CHANGE_ICON = File.join(File.expand_path("images", ::Environment::ROOTDIR), "change_icon.jpg")
5
+
6
+ def self.notify(title, msg, img=CHANGE_ICON)
7
+ options = "-t #{EXPIRATION_IN_SECONDS * 1000} -i #{img}"
8
+ system "notify-send #{options} '#{title}' '#{msg}'"
9
+ end
10
+
11
+ end
@@ -0,0 +1,11 @@
1
+ module Autotest::Growl
2
+
3
+ EXPIRATION_IN_SECONDS = 2
4
+ dir = File.dirname(__FILE__)
5
+ CHANGE_ICON = File.expand_path(dir + "/../images/change_icon.jpg")
6
+
7
+ def self.growl(title, msg, img=CHANGE_ICON, pri=0, stick="")
8
+ system "growlnotify -n autotest --image #{img} -p #{pri} -m #{msg.inspect} #{title} #{stick}"
9
+ end
10
+
11
+ end
@@ -0,0 +1,73 @@
1
+ module Notification
2
+
3
+ class Finder
4
+
5
+ def self.find_notify
6
+ if RUBY_PLATFORM =~ /mswin/
7
+ return
8
+ elsif RUBY_PLATFORM =~ /linux/
9
+ libnotify = system('locate libnotify-bin > /dev/null')
10
+
11
+ if libnotify && LibChecker.autotest
12
+ require_relative "gnomenotify"
13
+ return "libnotify"
14
+ elsif LibChecker.autotest_notification
15
+ require_relative "growl"
16
+ return "growl"
17
+ else
18
+ return
19
+ end
20
+
21
+ elsif RUBY_PLATFORM =~ /darwin/i
22
+
23
+ if LibChecker.autotest_notification
24
+ require_relative "growl"
25
+ return "growl"
26
+ else
27
+ return
28
+ end
29
+
30
+ end
31
+ end
32
+
33
+ end
34
+
35
+ class LibChecker
36
+
37
+ class << self
38
+ def autotest
39
+ begin
40
+ require 'autotest'
41
+ rescue LoadError
42
+ if require 'rubygems'
43
+ retry
44
+ else
45
+ return
46
+ end
47
+ end
48
+ if defined? Autotest
49
+ return true
50
+ end
51
+ end
52
+
53
+
54
+ def autotest_notification
55
+ begin
56
+ require 'autotest_notification'
57
+ rescue LoadError
58
+ if require 'rubygems'
59
+ retry
60
+ else
61
+ return
62
+ end
63
+ end
64
+ if defined? Autotest
65
+ return true
66
+ end
67
+ end
68
+ end
69
+
70
+ end
71
+
72
+ end # end module
73
+
@@ -0,0 +1,634 @@
1
+ module Server
2
+ # standard lib.
3
+ require 'socket'
4
+ require 'open-uri'
5
+ require 'yaml'
6
+
7
+ # vendor
8
+ begin
9
+ require 'nokogiri'
10
+ rescue LoadError
11
+ if require 'rubygems'
12
+ retry
13
+ else
14
+ abort 'Nokogiri is needed to run hubeye. Gem install nokogiri'
15
+ end
16
+ end
17
+
18
+ # hubeye
19
+ require "notification/notification"
20
+ require "log/logger"
21
+ require "timehelper/timehelper"
22
+ require "hooks/git_hooks"
23
+ require "hooks/executer"
24
+
25
+ # ONCEAROUND: 30 (seconds) is the default amount of time for looking
26
+ # for changes in every single repository. If tracking lots of repos,
27
+ # it might be a good idea to increase the value, or hubeye will cry
28
+ # due to overwork, fatigue and general anhedonia.
29
+ ONCEAROUND = 30
30
+ # USERNAME: defined in ~/.hubeyerc
31
+ USERNAME = 'luke-gru'
32
+ # find Desktop notification system
33
+ DESKTOP_NOTIFICATION = Notification::Finder.find_notify
34
+
35
+ class InputError < StandardError; end
36
+
37
+ def start(port, options={})
38
+ listen(port)
39
+ setup_env(options)
40
+ loop do
41
+ catch(:next) do
42
+ not_connected() unless @remote_connection
43
+ get_input(@socket)
44
+ puts @input if @debug
45
+ parse_input()
46
+ get_github_doc("/#{@username}/#{@repo_name}")
47
+ parse_doc()
48
+ @username = USERNAME
49
+ end
50
+ end
51
+ end
52
+
53
+ # Listen on port (2000 is the default)
54
+ def listen(port)
55
+ @server = TCPServer.open(port)
56
+ end
57
+
58
+
59
+ def setup_env(options={})
60
+ @daemonized = options[:daemon]
61
+ @sockets = [@server] # An array of sockets we'll monitor
62
+ @ary_commits_repos = []
63
+ @hubeye_tracker = []
64
+ # @username changes if input includes a '/' when removing, adding tracked
65
+ # repos.
66
+ @username = 'luke-gru'
67
+ @remote_connection = false
68
+ end
69
+
70
+
71
+ def not_connected
72
+ # if no client is connected, but the commits array contains repos
73
+ if @sockets.size == 1 and @ary_commits_repos.empty? == false
74
+
75
+ while @remote_connection == false
76
+ @hubeye_tracker.each do |repo|
77
+ doc = Nokogiri::HTML(open("https://github.com/#{repo}/"))
78
+
79
+ # make variable not block-local
80
+ commit_msg = nil
81
+
82
+ doc.xpath('//div[@class = "message"]/pre').each do |node|
83
+ commit_msg = node.text
84
+ if @ary_commits_repos.include?(commit_msg)
85
+ ONCEAROUND.times do
86
+ sleep 1
87
+ @remote_connection = client_ready(@sockets) ? true : false
88
+ break if @remote_connection
89
+ end
90
+ else
91
+ # There was a change to a tracked repository.
92
+
93
+ # make variable not block-local
94
+ committer = nil
95
+
96
+ doc.xpath('//div[@class = "actor"]/div[@class = "name"]').each do |node|
97
+ committer = node.text
98
+ end
99
+
100
+ # notify of change to repository
101
+ # if they have a Desktop notification
102
+ # library installed
103
+ change_msg = "Repo #{repo} has changed\nNew commit: #{commit_msg} => #{committer}"
104
+ case DESKTOP_NOTIFICATION
105
+ when "libnotify"
106
+ Autotest::GnomeNotify.notify("Hubeye", change_msg)
107
+ Logger.log_change(repo, commit_msg, committer)
108
+ when "growl"
109
+ Autotest::Growl.growl("Hubeye", change_msg)
110
+ Logger.log_change(repo, commit_msg, committer)
111
+ when nil
112
+
113
+ if @daemonized
114
+ Logger.log_change(repo, commit_msg, committer)
115
+ else
116
+ Logger.log_change(repo, commit_msg, committer, :include_terminal => true)
117
+ end
118
+
119
+ end
120
+ # execute any hooks for that repository
121
+ unless @hook_cmds.nil? || @hook_cmds.empty?
122
+ if @hook_cmds[repo]
123
+ hook_cmds = @hook_cmds[repo].dup
124
+ dir = (hook_cmds.include?('/') ? hook_cmds.shift : nil)
125
+
126
+ # execute() takes [commands], {options} where
127
+ # options = :directory and :repo
128
+ Hooks::Command.execute(hook_cmds, :directory => dir, :repo => repo)
129
+ end
130
+ end
131
+
132
+ @ary_commits_repos << repo
133
+ @ary_commits_repos << commit_msg
134
+ # delete the repo and old commit that appear first in the array
135
+ index_old_HEAD = @ary_commits_repos.index(repo)
136
+ @ary_commits_repos.delete_at(index_old_HEAD)
137
+ # and again to get rid of the commit message
138
+ @ary_commits_repos.delete_at(index_old_HEAD)
139
+ end
140
+ end
141
+ end
142
+ redo unless @remote_connection
143
+ end # end of (while remote_connection == false)
144
+ end
145
+ client_connect(@sockets)
146
+ end
147
+
148
+
149
+ def client_ready(sockets)
150
+ select(sockets, nil, nil, 2)
151
+ end
152
+ private :client_ready
153
+
154
+
155
+ def client_connect(sockets)
156
+ ready = select(sockets)
157
+ readable = ready[0] # These sockets are readable
158
+ readable.each do |socket| # Loop through readable sockets
159
+ if socket == @server # If the server socket is ready
160
+ client = @server.accept # Accept a new client
161
+ @socket = client # From here on in referred to as @socket
162
+ sockets << @socket # Add it to the set of sockets
163
+ # Inform the client of connection
164
+ if !@hubeye_tracker.empty?
165
+ @socket.puts "Hubeye running on #{Socket.gethostname}\nTracking:#{@hubeye_tracker.join(' ')}"
166
+ else
167
+ @socket.puts "Hubeye running on #{Socket.gethostname}"
168
+ end
169
+
170
+ if !@daemonized
171
+ puts "Client connected at #{::TimeHelper::NOW}"
172
+ end
173
+
174
+ @socket.flush
175
+ # And log the fact that the client connected
176
+ if @still_logging == true
177
+ # if the client quit, do not wipe the log file
178
+ Logger.log "Accepted connection from #{@socket.peeraddr[2]} (#{::TimeHelper::NOW})"
179
+ else
180
+ # wipe the log file and start anew
181
+ Logger.relog "Accepted connection from #{@socket.peeraddr[2]} (#{::TimeHelper::NOW})"
182
+ end
183
+ Logger.log "local: #{@socket.addr}"
184
+ Logger.log "peer : #{@socket.peeraddr}"
185
+ end
186
+ end
187
+ end
188
+
189
+
190
+ def get_input(socket)
191
+ @input = socket.gets # Read input from the client
192
+ @input.chop! unless @input.nil? # Trim client's input
193
+ # If no input, the client has disconnected
194
+ if !@input
195
+ Logger.log "Client on #{socket.peeraddr[2]} disconnected."
196
+ @sockets.delete(socket) # Stop monitoring this socket
197
+ socket.close # Close it
198
+ throw(:next) # And go on to the next
199
+ end
200
+ end
201
+ private :get_input
202
+
203
+
204
+ def parse_input
205
+ @input.strip!; @input.downcase!
206
+ if parse_quit()
207
+ elsif parse_shutdown()
208
+ elsif save_hooks_or_repos()
209
+ elsif load_hooks_or_repos()
210
+ # parse_hook must be before parse_fullpath_add for the moment
211
+ elsif parse_hook()
212
+ elsif hook_list()
213
+ elsif tracking_list()
214
+ elsif parse_pwd()
215
+ elsif parse_empty()
216
+ elsif parse_remove()
217
+ elsif parse_fullpath_add()
218
+ elsif parse_add()
219
+ else
220
+ raise InputError "Invalid input #{@input}"
221
+ end
222
+ end
223
+
224
+
225
+ def parse_quit
226
+ if @input =~ /\Aquit|exit\Z/ # If the client asks to quit
227
+ @socket.puts("Bye!") # Say goodbye
228
+ Logger.log "Closing connection to #{@socket.peeraddr[2]}"
229
+ @remote_connection = false
230
+ if !@ary_commits_repos.empty?
231
+ Logger.log "Tracking: "
232
+ @ary_commits_repos.each do |repo|
233
+ Logger.log repo if @ary_commits_repos.index(repo).even?
234
+ end
235
+ end
236
+ Logger.log "" # to look pretty when multiple connections per loop
237
+ @sockets.delete(@socket) # Stop monitoring the socket
238
+ @socket.close # Terminate the session
239
+ # still_logging makes the server not wipe the log file
240
+ @still_logging = true
241
+ else
242
+ return
243
+ end
244
+ throw(:next)
245
+ end
246
+
247
+
248
+ def parse_shutdown
249
+ if @input == "shutdown"
250
+ # local
251
+ Logger.log "Closing connection to #{@socket.peeraddr[2]}"
252
+ Logger.log "Shutting down... (#{::TimeHelper::NOW})"
253
+ Logger.log ""
254
+ Logger.log ""
255
+ # peer
256
+ @socket.puts("Shutting down server")
257
+ else
258
+ return
259
+ end
260
+ shutdown()
261
+ end
262
+
263
+
264
+ def shutdown
265
+ @sockets.delete(@socket)
266
+ @socket.close
267
+ exit
268
+ end
269
+
270
+
271
+ def parse_hook
272
+ if %r{githook add} =~ @input
273
+ githook_add()
274
+ else
275
+ return
276
+ end
277
+ end
278
+
279
+
280
+ ##
281
+ # @hook_cmds:
282
+ # repo is the key, value is array of directory and commands. First element
283
+ # of array is the local directory for that remote repo, rest are commands
284
+ # related to hooks called on change of commit message (with plans to change
285
+ # that to commit SHA reference) of the remote repo
286
+ def githook_add
287
+ @input.gsub!(/diiv/, '/')
288
+ # make match-$globals parse input
289
+ @input =~ /add ([^\/]+\/\w+) (dir: (\S*) )?cmd: (.*)\Z/
290
+ @hook_cmds ||= {}
291
+ if $1 != nil && $4 != nil
292
+ if @hook_cmds[$1]
293
+ @hook_cmds[$1] << $4
294
+ elsif $2 != nil
295
+ @hook_cmds[$1] = [$3, $4]
296
+ else
297
+ @hook_cmds[$1] = [$4]
298
+ end
299
+ @socket.puts("Hook added")
300
+ else
301
+ @socket.puts("Format: 'githook add user/repo [dir: /my/dir/repo ] cmd: git pull origin'")
302
+ end
303
+ throw(:next)
304
+ end
305
+ private :githook_add
306
+
307
+
308
+ def save_hooks_or_repos
309
+ if @input =~ %r{\A\s*save hook(s?) as (.+)\Z}
310
+ if !@hook_cmds.nil? && !@hook_cmds.empty?
311
+ File.open("#{ENV['HOME']}/hublog/hooks/#{$2}.yml", "w") do |f_out|
312
+ ::YAML.dump(@hook_cmds, f_out)
313
+ end
314
+ @socket.puts("Saved hook#{$1} as #{$2}")
315
+ else
316
+ @socket.puts("No hook#{$1} to save")
317
+ end
318
+ throw(:next)
319
+ elsif @input =~ %r{\A\s*save repo(s?) as (.+)\Z}
320
+ if !@hubeye_tracker.empty?
321
+ File.open("#{ENV['HOME']}/hublog/repos/#{$2}.yml", "w") do |f_out|
322
+ ::YAML.dump(@hubeye_tracker, f_out)
323
+ end
324
+ @socket.puts("Saved repo#{$1} as #{$2}")
325
+ else
326
+ @socket.puts("No remote repos are being tracked")
327
+ end
328
+ throw(:next)
329
+ else
330
+ return
331
+ end
332
+ end
333
+
334
+
335
+ def load_hooks_or_repos
336
+ if @input =~ %r{\A\s*load hook(s?) (.+)\Z}
337
+ hookfile = "#{ENV['HOME']}/hublog/hooks/#{$2}.yml"
338
+
339
+ # establish non block-local scope
340
+ newhooks = nil
341
+
342
+ if File.exists?(hookfile)
343
+ File.open(hookfile) do |f|
344
+ newhooks = ::YAML.load(f)
345
+ end
346
+ @hook_cmds ||= {}
347
+ @hook_cmds = newhooks.merge(@hook_cmds)
348
+ @socket.puts("Loaded #{$1} #{$2}")
349
+ else
350
+ @socket.puts("No #{$1} file to load from")
351
+ end
352
+ throw(:next)
353
+ elsif @input =~ %r{\A\s*load repo(s)? (.+)\Z}
354
+ if File.exists? repo_file = "#{ENV['HOME']}/hublog/repos/#{$2}.yml"
355
+ newrepos = nil
356
+ File.open(repo_file) do |f|
357
+ newrepos = ::YAML.load(f)
358
+ end
359
+
360
+ if !newrepos
361
+ @socket.puts "Unable to load #{$2}: empty file"
362
+ throw(:next)
363
+ end
364
+ # newrepos is an array of repos to be tracked
365
+ newrepos.each do |e|
366
+ # append the repo and the newest commit message to the repos and
367
+ # commit messages array, then inform the client of the newest
368
+ # commit message
369
+ commit_msg = get_commit_msg(e)
370
+ # if the commit_msg is non-false, don't do anything, otherwise 'next'
371
+ commit_msg ? nil : next
372
+ @ary_commits_repos << e << commit_msg
373
+ # and append the repo to the hubeye_tracker array
374
+ @hubeye_tracker << e
375
+ end
376
+ @ary_commits_repos.uniq!
377
+ @hubeye_tracker.uniq!
378
+
379
+ @socket.puts "Loaded #{$2}.\nTracking:\n#{show_repos_pretty()}"
380
+ else
381
+ # no repo file with that name
382
+ @socket.puts("No file to load from")
383
+ end
384
+ throw(:next)
385
+ end
386
+ return
387
+ end
388
+
389
+
390
+ # helper method to get commit message for a
391
+ # single repo
392
+ def get_commit_msg(remote_repo)
393
+ begin
394
+ @doc = Nokogiri::HTML(open("https://github.com#{'/' + remote_repo}"))
395
+ rescue
396
+ return nil
397
+ end
398
+ # returns the commit message
399
+ commit_msg = parse_msg(@doc)
400
+ end
401
+
402
+
403
+ def show_repos_pretty
404
+ pretty_repos = ""
405
+ @ary_commits_repos.each do |e|
406
+ if @ary_commits_repos.index(e).even?
407
+ pretty_repos += e + "\n"
408
+ else
409
+ pretty_repos += " " + e + "\n"
410
+ end
411
+ end
412
+ pretty_repos
413
+ end
414
+
415
+
416
+ def hook_list
417
+ if @input =~ %r{hook list}
418
+ unless @hook_cmds.nil? || @hook_cmds.empty?
419
+ @hook_cmds.each do |repo, ary|
420
+ remote = repo
421
+ if ary.first.include? '/'
422
+ local = ary.first
423
+ cmds = ary[1..-1]
424
+ else
425
+ cmds = ary
426
+ local = "N/A"
427
+ end
428
+ format_string = <<-EOS
429
+ remote: #{remote}
430
+ dir : #{local}
431
+ cmds: #{cmds.each {|cmd| print cmd + ' ' }} \n
432
+ EOS
433
+ end
434
+ @socket.puts(format_string)
435
+ @socketspoke = true
436
+ end
437
+ else
438
+ return
439
+ end
440
+ @socket.puts("No hooks") unless @socketspoke
441
+ @socketspoke = nil
442
+ throw(:next)
443
+ end
444
+
445
+
446
+ # show the client what repos (with commit messages)
447
+ # they're tracking
448
+ def tracking_list
449
+ if @input =~ /\Atracking\s*\Z/
450
+ list = show_repos_pretty
451
+ @socket.puts(list)
452
+ throw(:next)
453
+ else
454
+ return
455
+ end
456
+ end
457
+
458
+
459
+ # This means the user pressed '.' in the client,
460
+ # wanting to track the pwd repo. The period is replaced
461
+ # by 'pwd' in the client application and sent to @input because of
462
+ # problems with the period getting stripped in TCP transit. The name
463
+ # of the client's present working directory comes right after the 'pwd'.
464
+ # Typing '.' in the client only works (ie: begins tracking the remote repo)
465
+ # if the root directory of the git repository has the same name as one of
466
+ # the user's github repositories.
467
+ def parse_pwd
468
+ if @input.match(/^pwd/)
469
+ @repo_name = @input[3..-1]
470
+ else
471
+ return
472
+ end
473
+ return true
474
+ end
475
+
476
+
477
+ def parse_empty
478
+ if @input == ''
479
+ @socket.puts("")
480
+ throw(:next)
481
+ else
482
+ return
483
+ end
484
+ end
485
+
486
+
487
+ # Like the method parse_pwd, in which the client application replaces
488
+ # the '.' with 'pwd' and sends that to this server instead, parse_remove
489
+ # does pretty much the same thing with '/'. This is because of the slash
490
+ # getting stripped in TCP transit. Here, the slash is replaced with 'diiv',
491
+ # as this is unlikely to be included anywhere in a real username/repository
492
+ # combination (or is it... duh duh DUUUUHH)
493
+ # p.s. no it isn't
494
+ def parse_remove
495
+ if %r{\Arm ([\w-](diiv)?[\w-]*)\Z} =~ @input
496
+ if $1.include?("diiv")
497
+ @username, @repo_name = $1.split('diiv')
498
+ else
499
+ @username, @repo_name = "#{@username}/#{$1}".split('/')
500
+ end
501
+
502
+ begin
503
+ index_found = @ary_commits_repos.index("#{@username}/#{@repo_name}")
504
+ if index_found
505
+ # consecutive indices in the array
506
+ for i in 1..2
507
+ @ary_commits_repos.delete_at(index_found)
508
+ end
509
+ @hubeye_tracker.delete("#{@username}/#{@repo_name}")
510
+ @socket.puts("Stopped watching repository #{@username}/#{@repo_name}")
511
+ sleep 0.5
512
+ throw(:next)
513
+ else
514
+ @socket.puts("Repository #{@username}/#{@repo_name} not currently being watched")
515
+ throw(:next)
516
+ end
517
+ rescue
518
+ @socket.puts($!)
519
+ throw(:next)
520
+ end
521
+ else
522
+ return
523
+ end
524
+ end
525
+
526
+
527
+ def parse_fullpath_add
528
+ if @input.include?('diiv')
529
+ # includes a '/', such as rails/rails, but in the adding to tracker context
530
+ @username, @repo_name = @input.split('diiv')
531
+ else
532
+ return
533
+ end
534
+ return true
535
+ end
536
+
537
+
538
+ # if the input is not the above special scenarios
539
+ def parse_add
540
+ @repo_name = @input
541
+ end
542
+
543
+
544
+ def get_github_doc(full_repo_path)
545
+ begin
546
+ # if adding a repo with another username
547
+ @doc = Nokogiri::HTML(open("https://github.com#{full_repo_path}"))
548
+ rescue OpenURI::HTTPError
549
+ @socket.puts("Not a Github repository!")
550
+ throw(:next)
551
+ rescue URI::InvalidURIError
552
+ @socket.puts("Not a valid URI")
553
+ throw(:next)
554
+ end
555
+ end
556
+
557
+
558
+ def parse_doc
559
+ # get commit msg
560
+ @commit_msg = parse_msg(@doc)
561
+ # get committer
562
+ @committer = parse_committer()
563
+
564
+ # new repo to track
565
+ full_repo_name = "#{@username}/#{@repo_name}"
566
+ if !@ary_commits_repos.include?(full_repo_name)
567
+ @ary_commits_repos << full_repo_name
568
+ @hubeye_tracker << full_repo_name
569
+ @ary_commits_repos << @commit_msg
570
+ # get commit info
571
+ @info = parse_info()
572
+ @msg = "#{@commit_msg} => #{@committer}".gsub(/\(author\)/, '')
573
+ # log the fact that the user added a repo to be tracked
574
+ Logger.log("Added to tracker: #{@ary_commits_repos[-2]} (#{::TimeHelper::NOW})")
575
+ # show the user, via the client, the info and commit msg for the commit
576
+ @socket.puts("#{@info}\n#{@msg}")
577
+
578
+ # new commit to tracked repo
579
+ elsif !@ary_commits_repos.include?(@commit_msg)
580
+ begin
581
+ index_of_msg = @ary_commits_repos.index(@username + "/" + @repo_name) + 1
582
+ @ary_commits_repos.delete_at(index_of_msg)
583
+ @ary_commits_repos.insert(index_of_msg - 1, @commit_msg)
584
+
585
+ # log to the logfile and tell the client
586
+ if @daemonized
587
+ Logger.log_change(@repo_name, @commit_msg, @committer,
588
+ :include_socket => true)
589
+ else
590
+ Logger.log_change(@repo_name, @commit_msg, @committer,
591
+ :include_socket => true, :include_terminal => true)
592
+ end
593
+ rescue
594
+ @socket.puts($!)
595
+ end
596
+ else
597
+ # no change to the tracked repo
598
+ @socket.puts("Repository #{@repo_name} has not changed")
599
+ end
600
+ end
601
+
602
+
603
+ def parse_msg(html_doc)
604
+ # get commit msg
605
+ html_doc.xpath('//div[@class = "message"]/pre').each do |node|
606
+ return commit_msg = node.text
607
+ end
608
+ end
609
+
610
+
611
+ def parse_committer
612
+ @doc.xpath('//div[@class = "actor"]/div[@class = "name"]').each do |node|
613
+ return committer = node.text
614
+ end
615
+ end
616
+
617
+
618
+ def parse_info
619
+ @doc.xpath('//div[@class = "machine"]').each do |node|
620
+ return info = node.text.gsub(/\n/, '').gsub(/tree/, "\ntree").gsub(/parent.*?(\w)/, "\nparent \\1").strip!
621
+ end
622
+ end
623
+
624
+ end # of Server module
625
+
626
+ class HubeyeServer
627
+ include Server
628
+
629
+ def initialize(debug=false)
630
+ @debug = debug
631
+ end
632
+
633
+ end
634
+
@@ -0,0 +1,3 @@
1
+ module TimeHelper
2
+ NOW = Time.now.strftime("%m/%d/%Y at %I:%M%p")
3
+ end
@@ -0,0 +1,17 @@
1
+ require File.join(File.dirname(__FILE__), "/../lib/notification/notification")
2
+
3
+ class NotifyTests < Test::Unit::TestCase
4
+
5
+ def test_libnotify_on_linux
6
+ if RUBY_PLATFORM =~ /linux/i
7
+ assert_equal "libnotify", ::Notification::Finder.find_notify
8
+ end
9
+ end
10
+
11
+ def test_growl_returns_on_darwin
12
+ if RUBY_PLATFORM =~ /darwin/i
13
+ assert_equal "growl", ::Notification::Finder.find_notify
14
+ end
15
+ end
16
+
17
+ end
data/test/test.rb ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require 'test/unit'
3
+ require_relative "notification"
4
+ require File.join(File.dirname(__FILE__), "/../lib/environment")
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hubeye
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Luke Gruber
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-08-31 00:00:00.000000000 -04:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: nokogiri
17
+ requirement: &69555570 !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ! '>='
21
+ - !ruby/object:Gem::Version
22
+ version: '0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: *69555570
26
+ description: Github repository commit watcher -- keep your eye on new commits from
27
+ multiple repos through an interactive CLI
28
+ email: luke.gru@gmail.com
29
+ executables:
30
+ - hubeye
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - lib/client/hubeye_client.rb
35
+ - lib/notification/growl.rb
36
+ - lib/notification/gnomenotify.rb
37
+ - lib/notification/notification.rb
38
+ - lib/timehelper/timehelper.rb
39
+ - lib/server/hubeye_server.rb
40
+ - lib/hooks/executer.rb
41
+ - lib/hooks/git_hooks.rb
42
+ - lib/log/logger.rb
43
+ - lib/environment.rb
44
+ - bin/hubeye
45
+ - Rakefile
46
+ - README.md
47
+ - VERSION.rb
48
+ - LICENSE
49
+ - test/test.rb
50
+ - test/notification.rb
51
+ has_rdoc: true
52
+ homepage:
53
+ licenses:
54
+ - MIT
55
+ post_install_message:
56
+ rdoc_options: []
57
+ require_paths:
58
+ - lib
59
+ required_ruby_version: !ruby/object:Gem::Requirement
60
+ none: false
61
+ requirements:
62
+ - - ! '>='
63
+ - !ruby/object:Gem::Version
64
+ version: 1.8.7
65
+ required_rubygems_version: !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ! '>='
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ requirements: []
72
+ rubyforge_project:
73
+ rubygems_version: 1.6.2
74
+ signing_key:
75
+ specification_version: 3
76
+ summary: Github repository commit watcher -- keep your eye on new commits from multiple
77
+ repos through an interactive CLI
78
+ test_files: []