ghundle 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +5 -0
- data/README.md +86 -0
- data/bin/ghundle +21 -0
- data/lib/ghundle/app_error.rb +7 -0
- data/lib/ghundle/command/common.rb +70 -0
- data/lib/ghundle/command/fetch.rb +33 -0
- data/lib/ghundle/command/install.rb +93 -0
- data/lib/ghundle/command/list_all.rb +22 -0
- data/lib/ghundle/command/list_installed.rb +38 -0
- data/lib/ghundle/command/run.rb +20 -0
- data/lib/ghundle/command/uninstall.rb +29 -0
- data/lib/ghundle/command.rb +7 -0
- data/lib/ghundle/config.rb +19 -0
- data/lib/ghundle/hook.rb +28 -0
- data/lib/ghundle/hook_version.rb +23 -0
- data/lib/ghundle/main.rb +27 -0
- data/lib/ghundle/metadata.rb +25 -0
- data/lib/ghundle/options_parser.rb +54 -0
- data/lib/ghundle/source/common.rb +30 -0
- data/lib/ghundle/source/directory.rb +67 -0
- data/lib/ghundle/source/github.rb +99 -0
- data/lib/ghundle/source/local.rb +58 -0
- data/lib/ghundle/source.rb +3 -0
- data/lib/ghundle/version.rb +3 -0
- data/lib/ghundle.rb +0 -0
- metadata +113 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: bd51259b0ad3f91bf4ac2a51ee626477b325c19f
|
4
|
+
data.tar.gz: ccf45d5d461a0911f5b00a64de1b0a3bd7555a41
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4ad0bcfadce7edbcf088b0f0914fe26cdcb95873210f585662cec2750eb2f9ecbd6f0ef837d638af1ce2ca735b74c83144fe0df528d05954fa4ed0c1f12ab3f5
|
7
|
+
data.tar.gz: d4d9b6d863969ef095188bbf221bf7fd9928a16723115bd28509e444f97e3646813047338456d22ccfbdad8ad159fd4995347ed22333cb1cb1cd6bd54ebf1a09
|
data/LICENSE
ADDED
@@ -0,0 +1,5 @@
|
|
1
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
2
|
+
|
3
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
4
|
+
|
5
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
*This project is not completely done yet and the API is still in flux.*
|
2
|
+
|
3
|
+
## Usage
|
4
|
+
|
5
|
+
Fetch a hook from the local filesystem, useful for testing (see below for
|
6
|
+
directory format):
|
7
|
+
|
8
|
+
$ ghundle fetch ~/projects/hooks/ctags
|
9
|
+
>> Copying hook to ~/.ghundle/ctags...
|
10
|
+
|
11
|
+
Fetch a hook from a remote github repo:
|
12
|
+
|
13
|
+
$ ghundle fetch github.com/AndrewRadev/my-hooks-repo/ctags
|
14
|
+
>> Copying hook to ~/.ghundle/ctags...
|
15
|
+
|
16
|
+
List all available hooks:
|
17
|
+
|
18
|
+
$ ghundle list-all
|
19
|
+
|
20
|
+
ctags
|
21
|
+
- types: post-checkout
|
22
|
+
- description: Regenerates a project's tag files whenever a `git checkout` is run.
|
23
|
+
|
24
|
+
ruby-bundler
|
25
|
+
- types: post-merge, post-rewrite
|
26
|
+
- description: Runs a `bundle install` on every merge (this includes pulls).
|
27
|
+
|
28
|
+
<hook-name>
|
29
|
+
- type: <type>
|
30
|
+
- description: <description>
|
31
|
+
|
32
|
+
List all hooks, installed in the project:
|
33
|
+
|
34
|
+
$ ghundle list-installed
|
35
|
+
|
36
|
+
ctags
|
37
|
+
- types: post-checkout
|
38
|
+
- description: Regenerates a project's tag files whenever a `git checkout` is run.
|
39
|
+
|
40
|
+
Install a new hook in the project from the ghundle storage in `~/.ghundle`
|
41
|
+
(this automatically fetches if given a fetch-compatible url):
|
42
|
+
|
43
|
+
$ ghundle install ruby-bundler
|
44
|
+
$ ghundle install <hook-name>
|
45
|
+
|
46
|
+
$ ghundle install github.com/AndrewRadev/my-hooks-repo/ctags
|
47
|
+
$ ghundle install <anything that `ghundle fetch` accepts>
|
48
|
+
|
49
|
+
Uninstall a hook:
|
50
|
+
|
51
|
+
$ ghundle uninstall ruby-bundler
|
52
|
+
$ ghundle uninstall <hook-name>
|
53
|
+
|
54
|
+
Run a hook manually (it would need some arguments to work, see `man ghundle`):
|
55
|
+
|
56
|
+
$ ghundle run rails-migrations <args>
|
57
|
+
|
58
|
+
## Internals
|
59
|
+
|
60
|
+
The format of the source of a ghundle hook is a directory with the following
|
61
|
+
structure:
|
62
|
+
|
63
|
+
hook-name/
|
64
|
+
meta.yml
|
65
|
+
run
|
66
|
+
|
67
|
+
After running `ghundle fetch hook-name`, the `run` file and the metadata in
|
68
|
+
`meta.yml` will be processed and stored in `~/.ghundle`. The `run` file is the
|
69
|
+
actual script to run and it can be written any way you like. The `meta.yml`
|
70
|
+
file contains metadata and should have the form:
|
71
|
+
|
72
|
+
``` yaml
|
73
|
+
---
|
74
|
+
types: [<hook-type1>, <hook-type2>, ...]
|
75
|
+
version: <major>.<minor>.<patch>
|
76
|
+
description: <description of the hook's effect>
|
77
|
+
```
|
78
|
+
|
79
|
+
Each hook is written to the relevant `.git/hooks/*` file. For example, with the
|
80
|
+
abovementioned `ruby-bundler` and `rails-migrations`
|
81
|
+
would result in the `.git/hooks/post-merge` file looking like this:
|
82
|
+
|
83
|
+
## Start of ghundle scripts
|
84
|
+
ghundle run ruby-bundler $*
|
85
|
+
ghundle run rails-migrations $*
|
86
|
+
## End of ghundle scripts
|
data/bin/ghundle
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
|
3
|
+
$: << File.expand_path('../../lib', __FILE__)
|
4
|
+
|
5
|
+
require 'ghundle/main'
|
6
|
+
require 'ghundle/options_parser'
|
7
|
+
|
8
|
+
options_parser = Ghundle::OptionsParser.new(ARGV)
|
9
|
+
_options = options_parser.parse
|
10
|
+
|
11
|
+
if ARGV.length < 1
|
12
|
+
puts options_parser.usage
|
13
|
+
exit 1
|
14
|
+
end
|
15
|
+
|
16
|
+
begin
|
17
|
+
Ghundle::Main.exec(*ARGV.dup)
|
18
|
+
rescue Ghundle::AppError => e
|
19
|
+
puts e.message
|
20
|
+
exit 1
|
21
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'ghundle/config'
|
2
|
+
require 'rainbow'
|
3
|
+
|
4
|
+
module Ghundle
|
5
|
+
module Command
|
6
|
+
# Provides the basic interface of a Ghundle command. Includes some helper
|
7
|
+
# methods that are available in all command objects.
|
8
|
+
#
|
9
|
+
# Inheritors are expected to override the #call instance method, and they
|
10
|
+
# will have access to the command-line arguments through the #args method.
|
11
|
+
#
|
12
|
+
class Common
|
13
|
+
attr_reader :args
|
14
|
+
|
15
|
+
def self.call(*args)
|
16
|
+
new(*args).call
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(*args)
|
20
|
+
@args = args
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def hook_description(hook)
|
26
|
+
[
|
27
|
+
hook.name.foreground(:yellow),
|
28
|
+
" - version: #{hook.metadata.version}",
|
29
|
+
" - types: #{hook.metadata.types.join(', ')}",
|
30
|
+
" - description: #{hook.metadata.description}",
|
31
|
+
].join("\n")
|
32
|
+
end
|
33
|
+
|
34
|
+
def say(message)
|
35
|
+
print '>> '.foreground(:green).bright
|
36
|
+
puts message.foreground(:green)
|
37
|
+
end
|
38
|
+
|
39
|
+
def error(*args)
|
40
|
+
raise AppError.new(*args)
|
41
|
+
end
|
42
|
+
|
43
|
+
def config
|
44
|
+
@config ||= Config
|
45
|
+
end
|
46
|
+
|
47
|
+
def possible_hook_types
|
48
|
+
%w{
|
49
|
+
applypatch-msg
|
50
|
+
commit-msg
|
51
|
+
post-applypatch
|
52
|
+
post-checkout
|
53
|
+
post-commit
|
54
|
+
post-merge
|
55
|
+
post-receive
|
56
|
+
post-rewrite
|
57
|
+
post-update
|
58
|
+
pre-applypatch
|
59
|
+
pre-auto-gc
|
60
|
+
pre-commit
|
61
|
+
pre-push
|
62
|
+
pre-rebase
|
63
|
+
pre-receive
|
64
|
+
prepare-commit-msg
|
65
|
+
update
|
66
|
+
}
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'ghundle/command'
|
3
|
+
require 'ghundle/source'
|
4
|
+
require 'ghundle/hook'
|
5
|
+
|
6
|
+
module Ghundle
|
7
|
+
module Command
|
8
|
+
# Tries to figure out what kind of a source the given identifier represents
|
9
|
+
# and fetches the hook locally.
|
10
|
+
#
|
11
|
+
class Fetch < Common
|
12
|
+
def call
|
13
|
+
args.each do |identifier|
|
14
|
+
if identifier =~ /^github.com/
|
15
|
+
source = Source::Github.new(identifier)
|
16
|
+
elsif File.directory?(identifier)
|
17
|
+
source = Source::Directory.new(identifier)
|
18
|
+
elsif File.directory?(config.hook_path(identifier))
|
19
|
+
# already fetched, do nothing
|
20
|
+
return
|
21
|
+
else
|
22
|
+
error "Can't identify hook source from identifier: #{identifier}"
|
23
|
+
end
|
24
|
+
|
25
|
+
hook_name = source.hook_name
|
26
|
+
|
27
|
+
say "Fetching hook #{source.hook_name}..."
|
28
|
+
source.fetch(config.hook_path(source.hook_name))
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'ghundle/command'
|
2
|
+
|
3
|
+
module Ghundle
|
4
|
+
module Command
|
5
|
+
# Installs the given hook in the local repository. If there is no local
|
6
|
+
# hook by the given name, delegates to the the Fetch command to get the
|
7
|
+
# hook first.
|
8
|
+
#
|
9
|
+
class Install < Common
|
10
|
+
def call
|
11
|
+
args.each do |hook_name|
|
12
|
+
local_source = Source::Local.new(config.hook_path(hook_name))
|
13
|
+
|
14
|
+
if not local_source.exists?
|
15
|
+
# try to fetch it instead
|
16
|
+
Fetch.call(*args)
|
17
|
+
real_hook_name = File.basename(hook_name)
|
18
|
+
local_source = Source::Local.new(config.hook_path(real_hook_name))
|
19
|
+
end
|
20
|
+
|
21
|
+
hook = Hook.new(local_source)
|
22
|
+
|
23
|
+
prepare_git_hook(hook)
|
24
|
+
install_git_hook(hook)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def prepare_git_hook(hook)
|
29
|
+
validate_git_repo
|
30
|
+
|
31
|
+
hook_types = hook.metadata.types
|
32
|
+
hook_types.each do |hook_type|
|
33
|
+
validate_hook_type(hook_type)
|
34
|
+
end
|
35
|
+
|
36
|
+
hook_types.each do |hook_type|
|
37
|
+
git_hook_file = ".git/hooks/#{hook_type}"
|
38
|
+
|
39
|
+
if not File.exists?(git_hook_file)
|
40
|
+
File.open(git_hook_file, 'w') do |f|
|
41
|
+
f.puts '#! /bin/sh'
|
42
|
+
f.puts ''
|
43
|
+
end
|
44
|
+
File.chmod(0755, git_hook_file)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def install_git_hook(hook)
|
50
|
+
hook.metadata.types.each do |hook_type|
|
51
|
+
git_hook_file = ".git/hooks/#{hook_type}"
|
52
|
+
hook_invocation = "ghundle run #{hook.name} $*"
|
53
|
+
|
54
|
+
lines = File.readlines(git_hook_file).map(&:rstrip)
|
55
|
+
existing_hook = lines.find { |l| l.include?(hook_invocation) }
|
56
|
+
|
57
|
+
if existing_hook
|
58
|
+
say "Hook already installed for #{hook_type}"
|
59
|
+
return
|
60
|
+
end
|
61
|
+
|
62
|
+
say "Installing hook #{hook} for #{hook_type}"
|
63
|
+
end_line_index = lines.rindex { |l| l =~ /^# End of ghundle scripts/ }
|
64
|
+
|
65
|
+
if end_line_index
|
66
|
+
lines.insert(end_line_index, hook_invocation)
|
67
|
+
else
|
68
|
+
lines << '# Start of ghundle scripts'
|
69
|
+
lines << hook_invocation
|
70
|
+
lines << '# End of ghundle scripts'
|
71
|
+
end
|
72
|
+
|
73
|
+
File.open(git_hook_file, 'w') do |f|
|
74
|
+
f.write(lines.join("\n"))
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def validate_git_repo
|
80
|
+
if not File.directory?('.git/hooks')
|
81
|
+
error "Can't find `.git/hooks` directory, are you in a git repository?"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# TODO (2013-06-30) Move validations to Metadata?
|
86
|
+
def validate_hook_type(type)
|
87
|
+
if not possible_hook_types.include?(type)
|
88
|
+
error "The type of the script needs to be one of: #{possible_hook_types.join(', ')}."
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'ghundle/command'
|
2
|
+
|
3
|
+
module Ghundle
|
4
|
+
module Command
|
5
|
+
# Lists all available hooks stored in the hook home directory.
|
6
|
+
#
|
7
|
+
class ListAll < Common
|
8
|
+
def call
|
9
|
+
puts output.strip
|
10
|
+
end
|
11
|
+
|
12
|
+
def output
|
13
|
+
Dir[config.hooks_root.join('*')].map do |path|
|
14
|
+
next if not File.directory?(path)
|
15
|
+
|
16
|
+
hook = Hook.new(Source::Local.new(path))
|
17
|
+
hook_description(hook)
|
18
|
+
end.join("\n")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'ghundle/command'
|
3
|
+
|
4
|
+
module Ghundle
|
5
|
+
module Command
|
6
|
+
# Lists all hooks installed in the current repo.
|
7
|
+
#
|
8
|
+
class ListInstalled < Common
|
9
|
+
def call
|
10
|
+
puts output.strip
|
11
|
+
end
|
12
|
+
|
13
|
+
def output
|
14
|
+
hook_names = Set.new
|
15
|
+
|
16
|
+
Dir['.git/hooks/*'].each do |filename|
|
17
|
+
File.open(filename) do |f|
|
18
|
+
f.each_line do |line|
|
19
|
+
if line =~ /^ghundle run (.*) \$\*/
|
20
|
+
hook_names << $1.strip
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
hook_names.sort.map do |hook_name|
|
27
|
+
source = Source::Local.new(config.hook_path(hook_name))
|
28
|
+
|
29
|
+
if source.exists?
|
30
|
+
hook_description(Hook.new(source))
|
31
|
+
else
|
32
|
+
"Warning: Hook `#{hook_name}` does not exist".foreground(:red)
|
33
|
+
end
|
34
|
+
end.join("\n")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'ghundle/command'
|
2
|
+
require 'ghundle/app_error'
|
3
|
+
|
4
|
+
module Ghundle
|
5
|
+
module Command
|
6
|
+
# Runs the given hook, providing it with the rest of the positional
|
7
|
+
# arguments on the command-line.
|
8
|
+
#
|
9
|
+
class Run < Common
|
10
|
+
def call
|
11
|
+
name = args.first
|
12
|
+
hook_path = config.hook_path(name)
|
13
|
+
hook = Hook.new(Source::Local.new(hook_path))
|
14
|
+
|
15
|
+
say "Running hook #{hook.name}"
|
16
|
+
hook.run(*args[1 .. -1])
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'ghundle/command'
|
2
|
+
|
3
|
+
module Ghundle
|
4
|
+
module Command
|
5
|
+
# Uninstalls the given hook from the local repository.
|
6
|
+
#
|
7
|
+
class Uninstall < Common
|
8
|
+
def call
|
9
|
+
hook_name = args.first
|
10
|
+
hook_invocation = "ghundle run #{hook_name} $*\n"
|
11
|
+
|
12
|
+
containing_hooks = Dir['.git/hooks/*'].map do |filename|
|
13
|
+
contents = IO.read(filename)
|
14
|
+
[filename, contents] if contents.include? hook_invocation
|
15
|
+
end.compact
|
16
|
+
|
17
|
+
if containing_hooks.empty?
|
18
|
+
say "Hook #{hook_name} not installed"
|
19
|
+
end
|
20
|
+
|
21
|
+
containing_hooks.each do |filename, contents|
|
22
|
+
say "Deleting from hook file `#{filename}`..."
|
23
|
+
contents.gsub!(hook_invocation, '')
|
24
|
+
File.open(filename, 'w') { |f| f.write(contents) }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Ghundle
|
2
|
+
module Config
|
3
|
+
extend self
|
4
|
+
|
5
|
+
attr_accessor :hooks_root
|
6
|
+
|
7
|
+
def hooks_root
|
8
|
+
@hooks_root || Pathname.new(File.expand_path('~/.ghundle/'))
|
9
|
+
end
|
10
|
+
|
11
|
+
def hook_path(hook_name)
|
12
|
+
if not hooks_root.directory?
|
13
|
+
FileUtils.mkdir_p(hooks_root)
|
14
|
+
end
|
15
|
+
|
16
|
+
hooks_root.join(hook_name)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/ghundle/hook.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'ghundle/config'
|
2
|
+
|
3
|
+
module Ghundle
|
4
|
+
class Hook
|
5
|
+
attr_reader :source
|
6
|
+
|
7
|
+
def initialize(source)
|
8
|
+
@source = source
|
9
|
+
end
|
10
|
+
|
11
|
+
def name
|
12
|
+
source.name
|
13
|
+
end
|
14
|
+
|
15
|
+
def metadata
|
16
|
+
source.metadata
|
17
|
+
end
|
18
|
+
|
19
|
+
def run(*args)
|
20
|
+
source.validate
|
21
|
+
system source.script_path.to_s, *args
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_s
|
25
|
+
name
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Ghundle
|
2
|
+
class HookVersion
|
3
|
+
attr_reader :major, :minor, :patch
|
4
|
+
|
5
|
+
def initialize(string)
|
6
|
+
@major, @minor, @patch = string.split('.').map(&:to_i)
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_s
|
10
|
+
"#{major}.#{minor}.#{patch}"
|
11
|
+
end
|
12
|
+
|
13
|
+
def ==(other)
|
14
|
+
to_s == other.to_s
|
15
|
+
end
|
16
|
+
|
17
|
+
include Comparable
|
18
|
+
|
19
|
+
def <=>(other)
|
20
|
+
to_s <=> other.to_s
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/ghundle/main.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'ghundle/command'
|
2
|
+
|
3
|
+
module Ghundle
|
4
|
+
class Main
|
5
|
+
def self.exec(*args)
|
6
|
+
new(*args).exec
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(*args)
|
10
|
+
@command = args.shift
|
11
|
+
@args = args
|
12
|
+
end
|
13
|
+
|
14
|
+
def exec
|
15
|
+
case @command
|
16
|
+
when 'fetch' then Command::Fetch.call(*@args)
|
17
|
+
when 'install' then Command::Install.call(*@args)
|
18
|
+
when 'list-all' then Command::ListAll.call(*@args)
|
19
|
+
when 'list-installed' then Command::ListInstalled.call(*@args)
|
20
|
+
when 'run' then Command::Run.call(*@args)
|
21
|
+
when 'uninstall' then Command::Uninstall.call(*@args)
|
22
|
+
else
|
23
|
+
raise AppError.new("Unknown command: #{@command}")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'ghundle/hook_version'
|
2
|
+
|
3
|
+
module Ghundle
|
4
|
+
class Metadata
|
5
|
+
attr_reader :types, :version, :description
|
6
|
+
|
7
|
+
def self.from_yaml(filename)
|
8
|
+
new(YAML.load_file(filename))
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(data = {})
|
12
|
+
@version = HookVersion.new(data['version'])
|
13
|
+
@types = data['types']
|
14
|
+
@description = data['description']
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_h
|
18
|
+
{
|
19
|
+
'types' => types,
|
20
|
+
'version' => version.to_s,
|
21
|
+
'description' => description,
|
22
|
+
}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'ostruct'
|
3
|
+
require 'ghundle/version'
|
4
|
+
|
5
|
+
module Ghundle
|
6
|
+
# Contains the logic for extracting command-line options. Relies on
|
7
|
+
# 'optparse' from the standard library.
|
8
|
+
class OptionsParser
|
9
|
+
def initialize(args)
|
10
|
+
@args = args
|
11
|
+
end
|
12
|
+
|
13
|
+
def usage
|
14
|
+
[
|
15
|
+
'',
|
16
|
+
'Usage: ghundle <command> [options...]',
|
17
|
+
'',
|
18
|
+
'Commands:',
|
19
|
+
'',
|
20
|
+
' ghundle list-all',
|
21
|
+
' ghundle run <hook-name>',
|
22
|
+
' ghundle fetch github.com/<username>/<repo>/<path/to/hook/dir>',
|
23
|
+
' ghundle install <hook-name>',
|
24
|
+
' ghundle install github.com/<username>/<repo>/<path/to/hook/dir>',
|
25
|
+
' ghundle list-installed',
|
26
|
+
' ghundle uninstall <hook-name>',
|
27
|
+
'',
|
28
|
+
'Options:',
|
29
|
+
'',
|
30
|
+
].join("\n")
|
31
|
+
end
|
32
|
+
|
33
|
+
def parse
|
34
|
+
options = OpenStruct.new
|
35
|
+
|
36
|
+
parser = OptionParser.new do |o|
|
37
|
+
o.banner = usage
|
38
|
+
|
39
|
+
o.on_tail("-h", "--help", "Show this message") do
|
40
|
+
puts o
|
41
|
+
exit
|
42
|
+
end
|
43
|
+
|
44
|
+
o.on_tail("--version", "Show version") do
|
45
|
+
puts VERSION
|
46
|
+
exit
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
parser.parse!(@args)
|
51
|
+
options
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Ghundle
|
2
|
+
module Source
|
3
|
+
# A descendant of Source::Common is a source that can be used to fetch a
|
4
|
+
# hook from a remote location into a local path. Look at the various
|
5
|
+
# children of this class to understand its usage better.
|
6
|
+
#
|
7
|
+
class Common
|
8
|
+
# Gets the name of the hook based on the url/path it's given
|
9
|
+
def hook_name
|
10
|
+
raise NotImplementedError
|
11
|
+
end
|
12
|
+
|
13
|
+
# Returns this source's metadata object.
|
14
|
+
def metadata
|
15
|
+
raise NotImplementedError
|
16
|
+
end
|
17
|
+
|
18
|
+
# Fetches the source into the local path, making it ready for execution.
|
19
|
+
# Returns a Source::Local that can be used to access its data.
|
20
|
+
def fetch(destination_path)
|
21
|
+
raise NotImplementedError
|
22
|
+
end
|
23
|
+
|
24
|
+
# Checks if the source was already extracted to the given path.
|
25
|
+
def fetched?(destination_path)
|
26
|
+
raise NotImplementedError
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'pathname'
|
3
|
+
require 'ghundle/metadata'
|
4
|
+
require 'ghundle/source/common'
|
5
|
+
|
6
|
+
module Ghundle
|
7
|
+
module Source
|
8
|
+
# Represents a directory on the filesystem that has a hook-compatible
|
9
|
+
# directory structure. It needs to be fetched to the local hook root in
|
10
|
+
# order to use the hook.
|
11
|
+
#
|
12
|
+
class Directory < Common
|
13
|
+
attr_reader :source_path
|
14
|
+
|
15
|
+
def initialize(path)
|
16
|
+
@source_path = Pathname.new(path)
|
17
|
+
end
|
18
|
+
|
19
|
+
def hook_name
|
20
|
+
@source_path.basename
|
21
|
+
end
|
22
|
+
|
23
|
+
def metadata
|
24
|
+
@metadata ||=
|
25
|
+
begin
|
26
|
+
validate
|
27
|
+
Metadata.new(YAML.load_file(source_path.join("meta.yml")))
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def fetch(destination_path)
|
32
|
+
validate
|
33
|
+
destination_path = Pathname.new(destination_path)
|
34
|
+
|
35
|
+
local_source = Local.new(destination_path)
|
36
|
+
return local_source if local_source.exists?
|
37
|
+
|
38
|
+
FileUtils.mkdir_p(destination_path)
|
39
|
+
FileUtils.cp source_path.join("meta.yml"), destination_path.join("meta.yml")
|
40
|
+
FileUtils.cp source_path.join("run"), destination_path.join("run")
|
41
|
+
|
42
|
+
local_source
|
43
|
+
end
|
44
|
+
|
45
|
+
def validate
|
46
|
+
source_script_path = source_path.join('run')
|
47
|
+
source_metadata_path = source_path.join('meta.yml')
|
48
|
+
|
49
|
+
if not source_script_path.file?
|
50
|
+
raise AppError.new("Script not found: #{source_script_path}")
|
51
|
+
end
|
52
|
+
|
53
|
+
if not source_script_path.executable?
|
54
|
+
raise AppError.new("Script not executable: #{source_script_path}")
|
55
|
+
end
|
56
|
+
|
57
|
+
if not source_metadata_path.file?
|
58
|
+
raise AppError.new("Metadata file not found: #{metadata_source_path}")
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def to_s
|
63
|
+
source_path
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'ghundle/metadata'
|
3
|
+
require 'ghundle/source/common'
|
4
|
+
|
5
|
+
module Ghundle
|
6
|
+
module Source
|
7
|
+
# Represents a remote hook on github.com. The description is of the format:
|
8
|
+
#
|
9
|
+
# github.com/<username>/<repo>/<path/to/hook>
|
10
|
+
#
|
11
|
+
# Example:
|
12
|
+
#
|
13
|
+
# github.com/AndrewRadev/hooks/ctags
|
14
|
+
#
|
15
|
+
# It needs to be fetched to the local hook root in order to use the hook.
|
16
|
+
#
|
17
|
+
class Github < Common
|
18
|
+
attr_reader :username, :repo, :path
|
19
|
+
attr_reader :script_name
|
20
|
+
|
21
|
+
def initialize(description)
|
22
|
+
@description = description
|
23
|
+
@username, @repo, @path = parse_description(@description)
|
24
|
+
@path = Pathname.new(@path)
|
25
|
+
@script_name = path.basename
|
26
|
+
end
|
27
|
+
|
28
|
+
def hook_name
|
29
|
+
path.basename
|
30
|
+
end
|
31
|
+
|
32
|
+
def metadata
|
33
|
+
@metadata ||=
|
34
|
+
begin
|
35
|
+
url = raw_github_url(@path.join('meta.yml'))
|
36
|
+
status, yaml = http_get(url)
|
37
|
+
|
38
|
+
if status != 200
|
39
|
+
raise AppError.new("Couldn't fetch metadata file from #{url}, got response status: #{status}")
|
40
|
+
end
|
41
|
+
|
42
|
+
Metadata.new(YAML.load(yaml))
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def fetch(destination_path)
|
47
|
+
destination_path = Pathname.new(destination_path)
|
48
|
+
|
49
|
+
local_source = Local.new(destination_path)
|
50
|
+
return local_source if local_source.exists?
|
51
|
+
|
52
|
+
FileUtils.mkdir_p(destination_path)
|
53
|
+
|
54
|
+
status, script = http_get(raw_github_url(@path.join('run')))
|
55
|
+
if status != 200
|
56
|
+
raise AppError.new("Couldn't fetch script file from #{url}, got response status: #{status}")
|
57
|
+
end
|
58
|
+
|
59
|
+
destination_path.join('run').open('w') do |f|
|
60
|
+
f.write(script)
|
61
|
+
end
|
62
|
+
|
63
|
+
destination_path.join('meta.yml').open('w') do |f|
|
64
|
+
f.write(YAML.dump(metadata.to_h))
|
65
|
+
end
|
66
|
+
|
67
|
+
File.chmod(0755, destination_path.join('run'))
|
68
|
+
|
69
|
+
local_source
|
70
|
+
end
|
71
|
+
|
72
|
+
def to_s
|
73
|
+
@path
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def http_get(url_string)
|
79
|
+
uri = URI(url_string)
|
80
|
+
response = Net::HTTP.get_response(uri)
|
81
|
+
|
82
|
+
[response.code.to_i, response.body]
|
83
|
+
end
|
84
|
+
|
85
|
+
def parse_description(description)
|
86
|
+
components = description.split('/')
|
87
|
+
username = components[1]
|
88
|
+
repo = components[2]
|
89
|
+
path = components[3..-1].join('/')
|
90
|
+
|
91
|
+
[username, repo, path]
|
92
|
+
end
|
93
|
+
|
94
|
+
def raw_github_url(path)
|
95
|
+
"https://raw.github.com/#{@username}/#{@repo}/master/#{path}"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require 'ghundle/metadata'
|
3
|
+
require 'ghundle/source/common'
|
4
|
+
|
5
|
+
module Ghundle
|
6
|
+
module Source
|
7
|
+
# Represents the local source of a hook. This means that the hook has
|
8
|
+
# already been fetched to a local directory where it can easily be
|
9
|
+
# installed in a repository.
|
10
|
+
#
|
11
|
+
class Local < Common
|
12
|
+
def initialize(local_path)
|
13
|
+
@local_path = Pathname.new(local_path)
|
14
|
+
end
|
15
|
+
|
16
|
+
def name
|
17
|
+
@local_path.basename.to_s
|
18
|
+
end
|
19
|
+
|
20
|
+
def validate
|
21
|
+
return if exists?
|
22
|
+
|
23
|
+
if not script_path.file?
|
24
|
+
raise AppError.new("Script not found: #{script_path}")
|
25
|
+
end
|
26
|
+
|
27
|
+
if not script_path.executable?
|
28
|
+
raise AppError.new("Script not executable: #{script_path}")
|
29
|
+
end
|
30
|
+
|
31
|
+
if not metadata_path.file?
|
32
|
+
raise AppError.new("Metadata file not found: #{metadata_path}")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def exists?
|
37
|
+
script_path.executable? and metadata_path.file?
|
38
|
+
end
|
39
|
+
|
40
|
+
def metadata
|
41
|
+
validate
|
42
|
+
Metadata.from_yaml(metadata_path)
|
43
|
+
end
|
44
|
+
|
45
|
+
def fetch
|
46
|
+
self
|
47
|
+
end
|
48
|
+
|
49
|
+
def script_path
|
50
|
+
@local_path.join('run')
|
51
|
+
end
|
52
|
+
|
53
|
+
def metadata_path
|
54
|
+
@local_path.join('meta.yml')
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
data/lib/ghundle.rb
ADDED
File without changes
|
metadata
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ghundle
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Andrew Radev
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-07-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rainbow
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 1.1.4
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 1.1.4
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 2.13.0
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 2.13.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description: |2
|
56
|
+
Lets you manage git hooks in your project and install them from remote
|
57
|
+
locations.
|
58
|
+
email:
|
59
|
+
- andrey.radev@gmail.com
|
60
|
+
executables:
|
61
|
+
- ghundle
|
62
|
+
extensions: []
|
63
|
+
extra_rdoc_files: []
|
64
|
+
files:
|
65
|
+
- lib/ghundle.rb
|
66
|
+
- lib/ghundle/command/common.rb
|
67
|
+
- lib/ghundle/command/run.rb
|
68
|
+
- lib/ghundle/command/install.rb
|
69
|
+
- lib/ghundle/command/list_installed.rb
|
70
|
+
- lib/ghundle/command/list_all.rb
|
71
|
+
- lib/ghundle/command/fetch.rb
|
72
|
+
- lib/ghundle/command/uninstall.rb
|
73
|
+
- lib/ghundle/hook_version.rb
|
74
|
+
- lib/ghundle/app_error.rb
|
75
|
+
- lib/ghundle/main.rb
|
76
|
+
- lib/ghundle/hook.rb
|
77
|
+
- lib/ghundle/command.rb
|
78
|
+
- lib/ghundle/source.rb
|
79
|
+
- lib/ghundle/config.rb
|
80
|
+
- lib/ghundle/options_parser.rb
|
81
|
+
- lib/ghundle/source/local.rb
|
82
|
+
- lib/ghundle/source/common.rb
|
83
|
+
- lib/ghundle/source/directory.rb
|
84
|
+
- lib/ghundle/source/github.rb
|
85
|
+
- lib/ghundle/version.rb
|
86
|
+
- lib/ghundle/metadata.rb
|
87
|
+
- bin/ghundle
|
88
|
+
- LICENSE
|
89
|
+
- README.md
|
90
|
+
homepage: http://github.com/AndrewRadev/ghundle
|
91
|
+
licenses: []
|
92
|
+
metadata: {}
|
93
|
+
post_install_message:
|
94
|
+
rdoc_options: []
|
95
|
+
require_paths:
|
96
|
+
- lib
|
97
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- - '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
103
|
+
requirements:
|
104
|
+
- - '>='
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: 1.3.6
|
107
|
+
requirements: []
|
108
|
+
rubyforge_project:
|
109
|
+
rubygems_version: 2.0.3
|
110
|
+
signing_key:
|
111
|
+
specification_version: 4
|
112
|
+
summary: A package manager for git hooks
|
113
|
+
test_files: []
|