hubeye 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.
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: []