jbox-gitolite 1.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gemtest +0 -0
- data/.gitignore +7 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.markdown +190 -0
- data/Rakefile +33 -0
- data/gitolite.gemspec +32 -0
- data/lib/gitolite.rb +8 -0
- data/lib/gitolite/config.rb +253 -0
- data/lib/gitolite/config/group.rb +51 -0
- data/lib/gitolite/config/repo.rb +94 -0
- data/lib/gitolite/dirty_proxy.rb +29 -0
- data/lib/gitolite/gitolite_admin.rb +206 -0
- data/lib/gitolite/ssh_key.rb +87 -0
- data/lib/gitolite/version.rb +3 -0
- data/spec/concurrency_spec.rb +2 -0
- data/spec/config_spec.rb +491 -0
- data/spec/dirty_proxy_spec.rb +63 -0
- data/spec/gitolite_admin_spec.rb +4 -0
- data/spec/group_spec.rb +126 -0
- data/spec/keys/bob-ins@zilla-site.com@desktop.pub +1 -0
- data/spec/keys/bob.joe@test.zilla.com@desktop.pub +1 -0
- data/spec/keys/bob.pub +1 -0
- data/spec/keys/bob@desktop.pub +1 -0
- data/spec/keys/bob@foo-bar.pub +1 -0
- data/spec/keys/bob@zilla.com.pub +1 -0
- data/spec/keys/bob@zilla.com@desktop.pub +1 -0
- data/spec/keys/jakub123.pub +1 -0
- data/spec/keys/jakub123@foo.net.pub +1 -0
- data/spec/keys/joe-bob@god-zilla.com@desktop.pub +1 -0
- data/spec/keys/joe@sch.ool.edu.pub +1 -0
- data/spec/keys/joe@sch.ool.edu@desktop.pub +1 -0
- data/spec/repo_spec.rb +204 -0
- data/spec/spec_helper.rb +2 -0
- data/spec/ssh_key_spec.rb +335 -0
- metadata +198 -0
@@ -0,0 +1,51 @@
|
|
1
|
+
module Gitolite
|
2
|
+
class Config
|
3
|
+
# Represents a group inside the gitolite configuration. The name and users
|
4
|
+
# options are all encapsulated in this class. All users are stored as
|
5
|
+
# Strings!
|
6
|
+
class Group
|
7
|
+
attr_accessor :name, :users
|
8
|
+
|
9
|
+
PREPEND_CHAR = '@'
|
10
|
+
|
11
|
+
def initialize(name)
|
12
|
+
# naively remove the prepend char
|
13
|
+
# I don't think you can have two of them in a group name
|
14
|
+
@name = name.gsub(PREPEND_CHAR, '')
|
15
|
+
@users = []
|
16
|
+
end
|
17
|
+
|
18
|
+
def empty!
|
19
|
+
@users.clear
|
20
|
+
end
|
21
|
+
|
22
|
+
def add_user(user)
|
23
|
+
return if has_user?(user)
|
24
|
+
@users.push(user.to_s).sort!
|
25
|
+
end
|
26
|
+
|
27
|
+
def add_users(*users)
|
28
|
+
fixed_users = users.flatten.map{ |u| u.to_s }
|
29
|
+
@users.concat(fixed_users).sort!.uniq!
|
30
|
+
end
|
31
|
+
|
32
|
+
def rm_user(user)
|
33
|
+
@users.delete(user.to_s)
|
34
|
+
end
|
35
|
+
|
36
|
+
def has_user?(user)
|
37
|
+
@users.include? user.to_s
|
38
|
+
end
|
39
|
+
|
40
|
+
def size
|
41
|
+
@users.length
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_s
|
45
|
+
members = @users.join(' ')
|
46
|
+
name = "#{PREPEND_CHAR}#{@name}"
|
47
|
+
"#{name.ljust(20)}= #{members}\n"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module Gitolite
|
2
|
+
class Config
|
3
|
+
#Represents a repo inside the gitolite configuration. The name, permissions, and git config
|
4
|
+
#options are all encapsulated in this class
|
5
|
+
class Repo
|
6
|
+
ALLOWED_PERMISSIONS = /-|C|R|RW\+?(?:C?D?|D?C?)M?/
|
7
|
+
|
8
|
+
attr_accessor :permissions, :name, :config, :options, :owner, :description
|
9
|
+
|
10
|
+
def initialize(name)
|
11
|
+
#Store the perm hash in a lambda since we have to create a new one on every deny rule
|
12
|
+
#The perm hash is stored as a 2D hash, with individual permissions being the first
|
13
|
+
#degree and individual refexes being the second degree. Both Hashes must respect order
|
14
|
+
@perm_hash_lambda = lambda { OrderedHash.new {|k,v| k[v] = OrderedHash.new{|k2, v2| k2[v2] = [] }} }
|
15
|
+
@permissions = Array.new.push(@perm_hash_lambda.call)
|
16
|
+
|
17
|
+
@name = name
|
18
|
+
@config = {} #git config
|
19
|
+
@options = {} #gitolite config
|
20
|
+
end
|
21
|
+
|
22
|
+
def clean_permissions
|
23
|
+
@permissions = Array.new.push(@perm_hash_lambda.call)
|
24
|
+
end
|
25
|
+
|
26
|
+
def add_permission(perm, refex = "", *users)
|
27
|
+
if perm =~ ALLOWED_PERMISSIONS
|
28
|
+
#Handle deny rules
|
29
|
+
if perm == '-'
|
30
|
+
@permissions.push(@perm_hash_lambda.call)
|
31
|
+
end
|
32
|
+
|
33
|
+
@permissions.last[perm][refex].concat users.flatten
|
34
|
+
@permissions.last[perm][refex].uniq!
|
35
|
+
else
|
36
|
+
raise InvalidPermissionError, "#{perm} is not in the allowed list of permissions!"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def set_git_config(key, value)
|
41
|
+
@config[key] = value
|
42
|
+
end
|
43
|
+
|
44
|
+
def unset_git_config(key)
|
45
|
+
@config.delete(key)
|
46
|
+
end
|
47
|
+
|
48
|
+
def set_gitolite_option(key, value)
|
49
|
+
@options[key] = value
|
50
|
+
end
|
51
|
+
|
52
|
+
def unset_gitolite_option(key)
|
53
|
+
@options.delete(key)
|
54
|
+
end
|
55
|
+
|
56
|
+
def to_s
|
57
|
+
repo = "repo #{@name}\n"
|
58
|
+
|
59
|
+
@permissions.each do |perm_hash|
|
60
|
+
perm_hash.each do |perm, list|
|
61
|
+
list.each do |refex, users|
|
62
|
+
repo += " " + perm.ljust(6) + refex.ljust(25) + "= " + users.join(' ') + "\n"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
@config.each do |k, v|
|
68
|
+
repo += " config " + k + " = " + v + "\n"
|
69
|
+
end
|
70
|
+
|
71
|
+
@options.each do |k, v|
|
72
|
+
repo += " option " + k + " = " + v + "\n"
|
73
|
+
end
|
74
|
+
|
75
|
+
repo
|
76
|
+
end
|
77
|
+
|
78
|
+
def gitweb_description
|
79
|
+
if @description.nil?
|
80
|
+
nil
|
81
|
+
else
|
82
|
+
desc = "#{@name} "
|
83
|
+
desc += "\"#{@owner}\" " unless @owner.nil?
|
84
|
+
desc += "= \"#{@description}\""
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
#Gets raised if a permission that isn't in the allowed
|
89
|
+
#list is passed in
|
90
|
+
class InvalidPermissionError < ArgumentError
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Gitolite
|
2
|
+
# Very simple proxy object for checking if the proxied object was modified
|
3
|
+
# since the last clean_up! method called. It works correctly only for objects
|
4
|
+
# with proper hash method!
|
5
|
+
|
6
|
+
class DirtyProxy < BasicObject
|
7
|
+
|
8
|
+
def initialize(target)
|
9
|
+
@target = target
|
10
|
+
clean_up!
|
11
|
+
end
|
12
|
+
|
13
|
+
def method_missing(method, *args, &block)
|
14
|
+
@target.send(method, *args, &block)
|
15
|
+
end
|
16
|
+
|
17
|
+
def respond_to?(symbol, include_private=false)
|
18
|
+
super || [:dirty?, :clean_up!].include?(symbol.to_sym)
|
19
|
+
end
|
20
|
+
|
21
|
+
def dirty?
|
22
|
+
@clean_hash != @target.hash
|
23
|
+
end
|
24
|
+
|
25
|
+
def clean_up!
|
26
|
+
@clean_hash = @target.hash
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,206 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), "dirty_proxy")
|
2
|
+
|
3
|
+
module Gitolite
|
4
|
+
class GitoliteAdmin
|
5
|
+
attr_accessor :gl_admin
|
6
|
+
|
7
|
+
CONF = "gitolite.conf"
|
8
|
+
CONFDIR = "conf"
|
9
|
+
KEYDIR = "keydir"
|
10
|
+
|
11
|
+
#Gitolite gem's default git commit message
|
12
|
+
DEFAULT_COMMIT_MSG = "Committed by the gitolite gem"
|
13
|
+
|
14
|
+
# Intialize with the path to
|
15
|
+
# the gitolite-admin repository
|
16
|
+
def initialize(path, options = {})
|
17
|
+
@path = path
|
18
|
+
@gl_admin = Grit::Repo.new(path)
|
19
|
+
|
20
|
+
@conf = options[:conf] || CONF
|
21
|
+
@confdir = options[:confdir] || CONFDIR
|
22
|
+
@keydir = options[:keydir] || KEYDIR
|
23
|
+
end
|
24
|
+
|
25
|
+
# This method will bootstrap a gitolite-admin repo
|
26
|
+
# at the given path. A typical gitolite-admin
|
27
|
+
# repo will have the following tree:
|
28
|
+
#
|
29
|
+
# gitolite-admin
|
30
|
+
# conf
|
31
|
+
# gitolite.conf
|
32
|
+
# keydir
|
33
|
+
def self.bootstrap(path, options = {})
|
34
|
+
if self.is_gitolite_admin_repo?(path)
|
35
|
+
if options[:overwrite]
|
36
|
+
FileUtils.rm_rf(File.join(path, '*'))
|
37
|
+
else
|
38
|
+
return self.new(path)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
FileUtils.mkdir_p([File.join(path,"conf"), File.join(path,"keydir")])
|
43
|
+
|
44
|
+
options[:perm] ||= "RW+"
|
45
|
+
options[:refex] ||= ""
|
46
|
+
options[:user] ||= "git"
|
47
|
+
|
48
|
+
c = Config.init
|
49
|
+
r = Config::Repo.new(options[:repo] || "gitolite-admin")
|
50
|
+
r.add_permission(options[:perm], options[:refex], options[:user])
|
51
|
+
c.add_repo(r)
|
52
|
+
config = c.to_file(File.join(path, "conf"))
|
53
|
+
|
54
|
+
repo = Grit::Repo.init(path)
|
55
|
+
Dir.chdir(path) do
|
56
|
+
repo.add(config)
|
57
|
+
repo.commit_index(options[:message] || "Config bootstrapped by the gitolite gem")
|
58
|
+
end
|
59
|
+
|
60
|
+
self.new(path)
|
61
|
+
end
|
62
|
+
|
63
|
+
#Writes all changed aspects out to the file system
|
64
|
+
#will also stage all changes
|
65
|
+
def save
|
66
|
+
Dir.chdir(@gl_admin.working_dir) do
|
67
|
+
#Process config file (if loaded, i.e. may be modified)
|
68
|
+
if @config
|
69
|
+
new_conf = @config.to_file(@confdir)
|
70
|
+
@gl_admin.add(new_conf)
|
71
|
+
end
|
72
|
+
|
73
|
+
#Process ssh keys (if loaded, i.e. may be modified)
|
74
|
+
if @ssh_keys
|
75
|
+
files = list_keys(@keydir).map{|f| File.basename f}
|
76
|
+
keys = @ssh_keys.values.map{|f| f.map {|t| t.filename}}.flatten
|
77
|
+
|
78
|
+
to_remove = (files - keys).map { |f| File.join(@keydir, f)}
|
79
|
+
@gl_admin.remove(to_remove)
|
80
|
+
|
81
|
+
@ssh_keys.each_value do |key|
|
82
|
+
#Write only keys from sets that has been modified
|
83
|
+
next if key.respond_to?(:dirty?) && !key.dirty?
|
84
|
+
key.each do |k|
|
85
|
+
@gl_admin.add(k.to_file(@keydir))
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# This method will destroy all local tracked changes, resetting the local gitolite
|
93
|
+
# git repo to HEAD and reloading the entire repository
|
94
|
+
# Note that this will also delete all untracked files
|
95
|
+
def reset!
|
96
|
+
Dir.chdir(@gl_admin.working_dir) do
|
97
|
+
@gl_admin.git.reset({:hard => true}, 'HEAD')
|
98
|
+
@gl_admin.git.clean({:d => true, :q => true, :f => true})
|
99
|
+
end
|
100
|
+
reload!
|
101
|
+
end
|
102
|
+
|
103
|
+
# This method will destroy the in-memory data structures and reload everything
|
104
|
+
# from the file system
|
105
|
+
def reload!
|
106
|
+
@ssh_keys = load_keys
|
107
|
+
@config = load_config
|
108
|
+
end
|
109
|
+
|
110
|
+
#commits all staged changes and pushes back
|
111
|
+
#to origin
|
112
|
+
#
|
113
|
+
#TODO: generate a better commit message
|
114
|
+
#TODO: add the ability to specify the remote and branch
|
115
|
+
#TODO: detect existance of origin instead of just dying
|
116
|
+
def apply(commit_message = DEFAULT_COMMIT_MSG)
|
117
|
+
@gl_admin.commit_index(commit_message)
|
118
|
+
@gl_admin.git.push({}, "origin", "master")
|
119
|
+
end
|
120
|
+
|
121
|
+
def save_and_apply(commit_message = DEFAULT_COMMIT_MSG)
|
122
|
+
self.save
|
123
|
+
self.apply(commit_message)
|
124
|
+
end
|
125
|
+
|
126
|
+
# Updates the repo with changes from remote master
|
127
|
+
def update(options = {})
|
128
|
+
options = {:reset => true, :rebase => false }.merge(options)
|
129
|
+
|
130
|
+
reset! if options[:reset]
|
131
|
+
|
132
|
+
Dir.chdir(@gl_admin.working_dir) do
|
133
|
+
@gl_admin.git.pull({:rebase => options[:rebase]}, "origin", "master")
|
134
|
+
end
|
135
|
+
|
136
|
+
reload!
|
137
|
+
end
|
138
|
+
|
139
|
+
def add_key(key)
|
140
|
+
raise "Key must be of type Gitolite::SSHKey!" unless key.instance_of? Gitolite::SSHKey
|
141
|
+
ssh_keys[key.owner] << key
|
142
|
+
end
|
143
|
+
|
144
|
+
def rm_key(key)
|
145
|
+
raise "Key must be of type Gitolite::SSHKey!" unless key.instance_of? Gitolite::SSHKey
|
146
|
+
ssh_keys[key.owner].delete key
|
147
|
+
end
|
148
|
+
|
149
|
+
#Checks to see if the given path is a gitolite-admin repository
|
150
|
+
#A valid repository contains a conf folder, keydir folder,
|
151
|
+
#and a configuration file within the conf folder
|
152
|
+
def self.is_gitolite_admin_repo?(dir)
|
153
|
+
# First check if it is a git repository
|
154
|
+
begin
|
155
|
+
Grit::Repo.new(dir)
|
156
|
+
rescue Grit::InvalidGitRepositoryError
|
157
|
+
return false
|
158
|
+
end
|
159
|
+
|
160
|
+
# If we got here it is a valid git repo,
|
161
|
+
# now check directory structure
|
162
|
+
File.exists?(File.join(dir, 'conf')) &&
|
163
|
+
File.exists?(File.join(dir, 'keydir')) &&
|
164
|
+
!Dir.glob(File.join(dir, 'conf', '*.conf')).empty?
|
165
|
+
end
|
166
|
+
|
167
|
+
def ssh_keys
|
168
|
+
@ssh_keys ||= load_keys
|
169
|
+
end
|
170
|
+
|
171
|
+
def config
|
172
|
+
@config ||= load_config
|
173
|
+
end
|
174
|
+
|
175
|
+
private
|
176
|
+
#Loads all .pub files in the gitolite-admin
|
177
|
+
#keydir directory
|
178
|
+
def load_keys(path = nil)
|
179
|
+
path ||= File.join(@path, @keydir)
|
180
|
+
keys = Hash.new {|k,v| k[v] = DirtyProxy.new([])}
|
181
|
+
|
182
|
+
list_keys(path).each do |key|
|
183
|
+
new_key = SSHKey.from_file(File.join(path, key))
|
184
|
+
owner = new_key.owner
|
185
|
+
|
186
|
+
keys[owner] << new_key
|
187
|
+
end
|
188
|
+
#Mark key sets as unmodified (for dirty checking)
|
189
|
+
keys.values.each{|set| set.clean_up!}
|
190
|
+
|
191
|
+
keys
|
192
|
+
end
|
193
|
+
|
194
|
+
def load_config(path = nil)
|
195
|
+
path ||= File.join(@path, @confdir, @conf)
|
196
|
+
Config.new(path)
|
197
|
+
end
|
198
|
+
|
199
|
+
def list_keys(path)
|
200
|
+
Dir.chdir(path) do
|
201
|
+
keys = Dir.glob("**/*.pub")
|
202
|
+
keys
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module Gitolite
|
2
|
+
#Models an SSH key within gitolite
|
3
|
+
#provides support for multikeys
|
4
|
+
#
|
5
|
+
#Types of multi keys:
|
6
|
+
# bob.pub => username: bob
|
7
|
+
# bob@desktop.pub => username: bob, location: desktop
|
8
|
+
# bob@email.com.pub => username: bob@email.com
|
9
|
+
# bob@email.com@desktop.pub => username: bob@email.com, location: desktop
|
10
|
+
|
11
|
+
class SSHKey
|
12
|
+
attr_accessor :owner, :location, :type, :blob, :email
|
13
|
+
|
14
|
+
def initialize(type, blob, email, owner = nil, location = "")
|
15
|
+
@type = type
|
16
|
+
@blob = blob
|
17
|
+
@email = email
|
18
|
+
|
19
|
+
@owner = owner || email
|
20
|
+
@location = location
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.from_file(key)
|
24
|
+
raise "#{key} does not exist!" unless File.exists?(key)
|
25
|
+
|
26
|
+
#Get our owner and location
|
27
|
+
File.basename(key) =~ /^([\w\.-]+(?:@(?:[\w-]+\.)+\D{2,4})?)(?:@([\w-]+))?.pub$/i
|
28
|
+
owner = $1
|
29
|
+
location = $2 || ""
|
30
|
+
|
31
|
+
# Use string key constructor
|
32
|
+
self.from_string(File.read(key), owner, location)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Construct a SSHKey from a string
|
36
|
+
def self.from_string(key_string, owner, location = "")
|
37
|
+
if owner.nil?
|
38
|
+
raise ArgumentError, "owner was nil, you must specify an owner"
|
39
|
+
end
|
40
|
+
|
41
|
+
#Get parts of the key
|
42
|
+
type, blob, email = key_string.split
|
43
|
+
|
44
|
+
# We need at least a type or blob
|
45
|
+
if type.nil? || blob.nil?
|
46
|
+
raise ArgumentError, "'#{key_string}' is not a valid SSH key string"
|
47
|
+
end
|
48
|
+
|
49
|
+
#If the key didn't have an email, just use the owner
|
50
|
+
if email.nil?
|
51
|
+
email = owner
|
52
|
+
end
|
53
|
+
|
54
|
+
self.new(type, blob, email, owner, location)
|
55
|
+
end
|
56
|
+
|
57
|
+
def to_s
|
58
|
+
[@type, @blob, @email].join(' ')
|
59
|
+
end
|
60
|
+
|
61
|
+
def to_file(path)
|
62
|
+
key_file = File.join(path, self.filename)
|
63
|
+
File.open(key_file, "w") do |f|
|
64
|
+
f.write(self.to_s)
|
65
|
+
end
|
66
|
+
key_file
|
67
|
+
end
|
68
|
+
|
69
|
+
def filename
|
70
|
+
file = @owner
|
71
|
+
file += "@#{@location}" unless @location.empty?
|
72
|
+
file += ".pub"
|
73
|
+
end
|
74
|
+
|
75
|
+
def ==(key)
|
76
|
+
@type == key.type &&
|
77
|
+
@blob == key.blob &&
|
78
|
+
@email == key.email &&
|
79
|
+
@owner == key.owner &&
|
80
|
+
@location == key.location
|
81
|
+
end
|
82
|
+
|
83
|
+
def hash
|
84
|
+
[@owner, @location, @type, @blob, @email].hash
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|