jbox-gitolite 1.1.2
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.
- 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
|