gitolite-rugged 1.2.pre.devel
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 +9 -0
- data/.travis.yml +14 -0
- data/Gemfile +5 -0
- data/Guardfile +13 -0
- data/LICENSE.txt +26 -0
- data/README.md +108 -0
- data/Rakefile +59 -0
- data/gitolite.gemspec +36 -0
- data/lib/gitolite/config/group.rb +62 -0
- data/lib/gitolite/config/repo.rb +107 -0
- data/lib/gitolite/config.rb +284 -0
- data/lib/gitolite/dirty_proxy.rb +32 -0
- data/lib/gitolite/gitolite_admin.rb +276 -0
- data/lib/gitolite/ssh_key.rb +103 -0
- data/lib/gitolite/version.rb +3 -0
- data/lib/gitolite.rb +10 -0
- data/spec/config_spec.rb +498 -0
- data/spec/dirty_proxy_spec.rb +66 -0
- data/spec/fixtures/configs/complicated-output.conf +72 -0
- data/spec/fixtures/configs/complicated.conf +311 -0
- data/spec/fixtures/configs/simple.conf +5 -0
- data/spec/fixtures/keys/bob+joe@test.zilla.com@desktop.pub +1 -0
- data/spec/fixtures/keys/bob-ins@zilla-site.com@desktop.pub +1 -0
- data/spec/fixtures/keys/bob.joe@test.zilla.com@desktop.pub +1 -0
- data/spec/fixtures/keys/bob.pub +1 -0
- data/spec/fixtures/keys/bob@desktop.pub +1 -0
- data/spec/fixtures/keys/bob@foo-bar.pub +1 -0
- data/spec/fixtures/keys/bob@zilla.com.pub +1 -0
- data/spec/fixtures/keys/bob@zilla.com@desktop.pub +1 -0
- data/spec/fixtures/keys/jakub123.pub +1 -0
- data/spec/fixtures/keys/jakub123@foo.net.pub +1 -0
- data/spec/fixtures/keys/joe-bob@god-zilla.com@desktop.pub +1 -0
- data/spec/fixtures/keys/joe@sch.ool.edu.pub +1 -0
- data/spec/fixtures/keys/joe@sch.ool.edu@desktop.pub +1 -0
- data/spec/gitolite_admin_spec.rb +40 -0
- data/spec/group_spec.rb +125 -0
- data/spec/repo_spec.rb +202 -0
- data/spec/spec_helper.rb +21 -0
- data/spec/ssh_key_spec.rb +355 -0
- metadata +280 -0
@@ -0,0 +1,284 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
|
3
|
+
module Gitolite
|
4
|
+
|
5
|
+
class Config
|
6
|
+
|
7
|
+
attr_accessor :repos, :groups, :filename
|
8
|
+
|
9
|
+
def self.init(filename = "gitolite.conf")
|
10
|
+
file = Tempfile.new(filename)
|
11
|
+
conf = self.new(file.path)
|
12
|
+
conf.filename = filename #kill suffix added by Tempfile
|
13
|
+
file.close(unlink_now = true)
|
14
|
+
conf
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
def initialize(config)
|
19
|
+
@repos = {}
|
20
|
+
@groups = {}
|
21
|
+
@filename = File.basename(config)
|
22
|
+
process_config(config)
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
# TODO: merge repo unless overwrite = true
|
27
|
+
def add_repo(repo, overwrite = false)
|
28
|
+
raise ArgumentError, "Repo must be of type Gitolite::Config::Repo!" unless repo.instance_of? Gitolite::Config::Repo
|
29
|
+
@repos[repo.name] = repo
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
def rm_repo(repo)
|
34
|
+
name = normalize_repo_name(repo)
|
35
|
+
@repos.delete(name)
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
def has_repo?(repo)
|
40
|
+
name = normalize_repo_name(repo)
|
41
|
+
@repos.has_key?(name)
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
def get_repo(repo)
|
46
|
+
name = normalize_repo_name(repo)
|
47
|
+
@repos[name]
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
def add_group(group, overwrite = false)
|
52
|
+
raise ArgumentError, "Group must be of type Gitolite::Config::Group!" unless group.instance_of? Gitolite::Config::Group
|
53
|
+
@groups[group.name] = group
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
def rm_group(group)
|
58
|
+
name = normalize_group_name(group)
|
59
|
+
@groups.delete(name)
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
def has_group?(group)
|
64
|
+
name = normalize_group_name(group)
|
65
|
+
@groups.has_key?(name)
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
def get_group(group)
|
70
|
+
name = normalize_group_name(group)
|
71
|
+
@groups[name]
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
def to_file(path=".", filename=@filename)
|
76
|
+
raise ArgumentError, "Path contains a filename or does not exist" unless File.directory?(path)
|
77
|
+
|
78
|
+
new_conf = File.join(path, filename)
|
79
|
+
File.open(new_conf, "w") do |f|
|
80
|
+
f.sync = true
|
81
|
+
|
82
|
+
# Output groups
|
83
|
+
dep_order = build_groups_depgraph
|
84
|
+
dep_order.each {|group| f.write group.to_s }
|
85
|
+
|
86
|
+
gitweb_descs = []
|
87
|
+
@repos.sort.each do |k, v|
|
88
|
+
f.write "\n"
|
89
|
+
f.write v.to_s
|
90
|
+
|
91
|
+
gwd = v.gitweb_description
|
92
|
+
gitweb_descs.push(gwd) unless gwd.nil?
|
93
|
+
end
|
94
|
+
|
95
|
+
f.write "\n"
|
96
|
+
f.write gitweb_descs.join("\n")
|
97
|
+
end
|
98
|
+
|
99
|
+
new_conf
|
100
|
+
end
|
101
|
+
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
|
106
|
+
# Based on
|
107
|
+
# https://github.com/sitaramc/gitolite/blob/pu/src/gl-compile-conf#cleanup_conf_line
|
108
|
+
def cleanup_config_line(line)
|
109
|
+
# remove comments, even those that happen inline
|
110
|
+
line.gsub!(/^((".*?"|[^#"])*)#.*/) {|m| m=$1}
|
111
|
+
|
112
|
+
# fix whitespace
|
113
|
+
line.gsub!('=', ' = ')
|
114
|
+
line.gsub!(/\s+/, ' ')
|
115
|
+
line.strip
|
116
|
+
end
|
117
|
+
|
118
|
+
|
119
|
+
def process_config(config)
|
120
|
+
context = [] #will store our context for permissions or config declarations
|
121
|
+
|
122
|
+
#Read each line of our config
|
123
|
+
File.open(config, 'r').each do |l|
|
124
|
+
|
125
|
+
line = cleanup_config_line(l)
|
126
|
+
next if line.empty? #lines are empty if we killed a comment
|
127
|
+
|
128
|
+
case line
|
129
|
+
|
130
|
+
# found a repo definition
|
131
|
+
when /^repo (.*)/
|
132
|
+
#Empty our current context
|
133
|
+
context = []
|
134
|
+
|
135
|
+
repos = $1.split
|
136
|
+
repos.each do |r|
|
137
|
+
context << r
|
138
|
+
|
139
|
+
@repos[r] = Repo.new(r) unless has_repo?(r)
|
140
|
+
end
|
141
|
+
|
142
|
+
# repo permissions
|
143
|
+
when /^(-|C|R|RW\+?(?:C?D?|D?C?)M?) (.* )?= (.+)/
|
144
|
+
perm = $1
|
145
|
+
refex = $2 || ""
|
146
|
+
users = $3.split
|
147
|
+
|
148
|
+
context.each do |c|
|
149
|
+
@repos[c].add_permission(perm, refex, users)
|
150
|
+
end
|
151
|
+
|
152
|
+
# repo git config
|
153
|
+
when /^config (.+) = ?(.*)/
|
154
|
+
key = $1
|
155
|
+
value = $2
|
156
|
+
|
157
|
+
context.each do |c|
|
158
|
+
@repos[c].set_git_config(key, value)
|
159
|
+
end
|
160
|
+
|
161
|
+
# repo gitolite option
|
162
|
+
when /^option (.+) = (.*)/
|
163
|
+
key = $1
|
164
|
+
value = $2
|
165
|
+
|
166
|
+
raise ParseError, "Missing gitolite option value for repo: #{repo} and key: #{key}" if value.nil?
|
167
|
+
|
168
|
+
context.each do |c|
|
169
|
+
@repos[c].set_gitolite_option(key, value)
|
170
|
+
end
|
171
|
+
|
172
|
+
# group definition
|
173
|
+
when /^#{Group::PREPEND_CHAR}(\S+) = ?(.*)/
|
174
|
+
group = $1
|
175
|
+
users = $2.split
|
176
|
+
|
177
|
+
@groups[group] = Group.new(group) unless has_group?(group)
|
178
|
+
@groups[group].add_users(users)
|
179
|
+
|
180
|
+
# gitweb definition
|
181
|
+
when /^(\S+)(?: "(.*?)")? = "(.*)"$/
|
182
|
+
repo = $1
|
183
|
+
owner = $2
|
184
|
+
description = $3
|
185
|
+
|
186
|
+
#Check for missing description
|
187
|
+
raise ParseError, "Missing Gitweb description for repo: #{repo}" if description.nil?
|
188
|
+
|
189
|
+
#Check for groups
|
190
|
+
raise ParseError, "Gitweb descriptions cannot be set for groups" if repo =~ /@.+/
|
191
|
+
|
192
|
+
if has_repo? repo
|
193
|
+
r = @repos[repo]
|
194
|
+
else
|
195
|
+
r = Repo.new(repo)
|
196
|
+
add_repo(r)
|
197
|
+
end
|
198
|
+
|
199
|
+
r.owner = owner
|
200
|
+
r.description = description
|
201
|
+
|
202
|
+
when /^include "(.+)"/
|
203
|
+
#TODO: implement includes
|
204
|
+
#ignore includes for now
|
205
|
+
|
206
|
+
when /^subconf (\S+)$/
|
207
|
+
#TODO: implement subconfs
|
208
|
+
#ignore subconfs for now
|
209
|
+
|
210
|
+
else
|
211
|
+
raise ParseError, "'#{line}' cannot be processed"
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
|
217
|
+
# Normalizes the various different input objects to Strings
|
218
|
+
def normalize_name(context, constant = nil)
|
219
|
+
case context
|
220
|
+
when constant
|
221
|
+
context.name
|
222
|
+
when Symbol
|
223
|
+
context.to_s
|
224
|
+
else
|
225
|
+
context
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
|
230
|
+
def method_missing(meth, *args, &block)
|
231
|
+
if meth.to_s =~ /normalize_(\w+)_name/
|
232
|
+
# Could use Object.const_get to figure out the constant here
|
233
|
+
# but for only two cases, this is more readable
|
234
|
+
case $1
|
235
|
+
when "repo"
|
236
|
+
normalize_name(args[0], Gitolite::Config::Repo)
|
237
|
+
when "group"
|
238
|
+
normalize_name(args[0], Gitolite::Config::Group)
|
239
|
+
end
|
240
|
+
else
|
241
|
+
super
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
|
246
|
+
# Builds a dependency tree from the groups in order to ensure all groups
|
247
|
+
# are defined before they are used
|
248
|
+
def build_groups_depgraph
|
249
|
+
dp = ::GRATR::Digraph.new
|
250
|
+
|
251
|
+
# Add each group to the graph
|
252
|
+
@groups.each_value do |group|
|
253
|
+
dp.add_vertex! group
|
254
|
+
|
255
|
+
# Select group names from the users
|
256
|
+
subgroups = group.users.select {|u| u =~ /^#{Group::PREPEND_CHAR}.*$/}.map{|g| get_group g.gsub(Group::PREPEND_CHAR, '') }
|
257
|
+
|
258
|
+
subgroups.each do |subgroup|
|
259
|
+
dp.add_edge! subgroup, group
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
# Figure out if we have a good depedency graph
|
264
|
+
dep_order = dp.topsort
|
265
|
+
|
266
|
+
if dep_order.empty?
|
267
|
+
raise GroupDependencyError unless @groups.empty?
|
268
|
+
end
|
269
|
+
|
270
|
+
dep_order
|
271
|
+
end
|
272
|
+
|
273
|
+
|
274
|
+
#Raised when something in a config fails to parse properly
|
275
|
+
class ParseError < RuntimeError
|
276
|
+
end
|
277
|
+
|
278
|
+
|
279
|
+
# Raised when group dependencies cannot be suitably resolved for output
|
280
|
+
class GroupDependencyError < RuntimeError
|
281
|
+
end
|
282
|
+
|
283
|
+
end
|
284
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Gitolite
|
2
|
+
|
3
|
+
# Very simple proxy object for checking if the proxied object was modified
|
4
|
+
# since the last clean_up! method called. It works correctly only for objects
|
5
|
+
# with proper hash method!
|
6
|
+
|
7
|
+
class DirtyProxy < BasicObject
|
8
|
+
|
9
|
+
def initialize(target)
|
10
|
+
@target = target
|
11
|
+
clean_up!
|
12
|
+
end
|
13
|
+
|
14
|
+
def method_missing(method, *args, &block)
|
15
|
+
@target.send(method, *args, &block)
|
16
|
+
end
|
17
|
+
|
18
|
+
def respond_to?(symbol, include_private=false)
|
19
|
+
super || [:dirty?, :clean_up!].include?(symbol.to_sym)
|
20
|
+
end
|
21
|
+
|
22
|
+
def dirty?
|
23
|
+
@clean_hash != @target.hash
|
24
|
+
end
|
25
|
+
|
26
|
+
def clean_up!
|
27
|
+
@clean_hash = @target.hash
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,276 @@
|
|
1
|
+
module Gitolite
|
2
|
+
class GitoliteAdmin
|
3
|
+
|
4
|
+
attr_accessor :repo
|
5
|
+
|
6
|
+
CONF_DIR = "conf"
|
7
|
+
KEY_DIR = "keydir"
|
8
|
+
|
9
|
+
CONFIG_FILE = "gitolite.conf"
|
10
|
+
CONFIG_PATH = File.join(CONF_DIR, "gitolite.conf")
|
11
|
+
|
12
|
+
|
13
|
+
# Default settings
|
14
|
+
DEFAULT_SETTINGS = {
|
15
|
+
# clone/push url settings
|
16
|
+
git_user: 'git',
|
17
|
+
hostname: 'localhost',
|
18
|
+
|
19
|
+
# Commit settings
|
20
|
+
author_name: 'gitolite-rugged gem',
|
21
|
+
author_email: 'gitolite-rugged@localhost',
|
22
|
+
commit_msg: 'Commited by the gitolite-rugged gem'
|
23
|
+
}
|
24
|
+
|
25
|
+
class << self
|
26
|
+
|
27
|
+
# Checks if the given path is a gitolite-admin repository
|
28
|
+
# A valid repository contains a conf folder, keydir folder,
|
29
|
+
# and a configuration file within the conf folder
|
30
|
+
def is_gitolite_admin_repo?(dir)
|
31
|
+
# First check if it is a git repository
|
32
|
+
begin
|
33
|
+
repo = Rugged::Repository.new(dir)
|
34
|
+
return false if repo.empty?
|
35
|
+
rescue Rugged::RepositoryError
|
36
|
+
return false
|
37
|
+
end
|
38
|
+
|
39
|
+
# Check if config file, key directory exist
|
40
|
+
[ File.join(dir, CONF_DIR), File.join(dir, KEY_DIR),
|
41
|
+
File.join(dir, CONFIG_PATH)
|
42
|
+
].each { |f| return false unless File.exists?(f) }
|
43
|
+
|
44
|
+
true
|
45
|
+
end
|
46
|
+
|
47
|
+
def admin_url(settings)
|
48
|
+
[settings[:git_user], '@', settings[:host], '/gitolite-admin.git'].join
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Intialize with the path to
|
53
|
+
# the gitolite-admin repository
|
54
|
+
#
|
55
|
+
# Settings:
|
56
|
+
# :git_user: The git user to SSH to (:git_user@localhost:gitolite-admin.git), defaults to 'git'
|
57
|
+
# :private_key: The key file containing the private SSH key for :git_user
|
58
|
+
# :public_key: The key file containing the public SSH key for :git_user
|
59
|
+
# :host: Hostname for clone url. Defaults to 'localhost'
|
60
|
+
# The settings hash is forwarded to +GitoliteAdmin.new+ as options.
|
61
|
+
def initialize(path, settings = {})
|
62
|
+
@path = path
|
63
|
+
@settings = DEFAULT_SETTINGS.merge(settings)
|
64
|
+
|
65
|
+
# Ensure SSH key settings exist
|
66
|
+
@settings.fetch(:public_key)
|
67
|
+
@settings.fetch(:private_key)
|
68
|
+
|
69
|
+
# setup credentials
|
70
|
+
@credentials = Rugged::Credentials::SshKey.new(
|
71
|
+
username: settings[:git_user], publickey: settings[:public_key],
|
72
|
+
privatekey: settings[:private_key] )
|
73
|
+
|
74
|
+
@repo =
|
75
|
+
if self.class.is_gitolite_admin_repo?(path)
|
76
|
+
Rugged::Repository.new(path)
|
77
|
+
else
|
78
|
+
clone
|
79
|
+
end
|
80
|
+
|
81
|
+
@config_file_path = File.join(@path, CONF_DIR, CONFIG_FILE)
|
82
|
+
@conf_dir_path = File.join(@path, CONF_DIR)
|
83
|
+
@key_dir_path = File.join(@path, KEY_DIR)
|
84
|
+
|
85
|
+
@commit_author = { email: settings[:author_email], name: settings[:author_name] }
|
86
|
+
|
87
|
+
reload!
|
88
|
+
end
|
89
|
+
|
90
|
+
def config
|
91
|
+
@config ||= load_config
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
def config=(config)
|
96
|
+
@config = config
|
97
|
+
end
|
98
|
+
|
99
|
+
|
100
|
+
def ssh_keys
|
101
|
+
@ssh_keys ||= load_keys
|
102
|
+
end
|
103
|
+
|
104
|
+
|
105
|
+
def add_key(key)
|
106
|
+
unless key.instance_of? Gitolite::SSHKey
|
107
|
+
raise GitoliteAdminError, "Key must be of type Gitolite::SSHKey!"
|
108
|
+
end
|
109
|
+
|
110
|
+
ssh_keys[key.owner] << key
|
111
|
+
end
|
112
|
+
|
113
|
+
|
114
|
+
def rm_key(key)
|
115
|
+
unless key.instance_of? Gitolite::SSHKey
|
116
|
+
raise GitoliteAdminError, "Key must be of type Gitolite::SSHKey!"
|
117
|
+
end
|
118
|
+
|
119
|
+
ssh_keys[key.owner].delete key
|
120
|
+
end
|
121
|
+
|
122
|
+
|
123
|
+
# This method will destroy all local tracked changes, resetting the local gitolite
|
124
|
+
# git repo to HEAD and reloading the entire repository
|
125
|
+
def reset!
|
126
|
+
@repo.reset('origin/master', :hard)
|
127
|
+
reload!
|
128
|
+
end
|
129
|
+
|
130
|
+
|
131
|
+
# This method will destroy the in-memory data structures and reload everything
|
132
|
+
# from the file system
|
133
|
+
def reload!
|
134
|
+
@ssh_keys = load_keys
|
135
|
+
@config = load_config
|
136
|
+
end
|
137
|
+
|
138
|
+
|
139
|
+
# Writes all changed aspects out to the file system
|
140
|
+
# will also stage all changes then commit
|
141
|
+
def save()
|
142
|
+
|
143
|
+
# Add all changes to index (staging area)
|
144
|
+
index = @repo.index
|
145
|
+
|
146
|
+
#Process config file (if loaded, i.e. may be modified)
|
147
|
+
if @config
|
148
|
+
new_conf = @config.to_file(@conf_dir_path)
|
149
|
+
|
150
|
+
# Rugged wants relative paths
|
151
|
+
index.add(CONFIG_PATH)
|
152
|
+
end
|
153
|
+
|
154
|
+
#Process ssh keys (if loaded, i.e. may be modified)
|
155
|
+
if @ssh_keys
|
156
|
+
files = list_keys.map{|f| File.basename f}
|
157
|
+
keys = @ssh_keys.values.map{|f| f.map {|t| t.filename}}.flatten
|
158
|
+
|
159
|
+
to_remove = (files - keys).map { |f| File.join(@key_dir, f) }
|
160
|
+
to_remove.each do |key|
|
161
|
+
File.unlink key
|
162
|
+
index.remove key
|
163
|
+
end
|
164
|
+
|
165
|
+
@ssh_keys.each_value do |key|
|
166
|
+
# Write only keys from sets that has been modified
|
167
|
+
next if key.respond_to?(:dirty?) && !key.dirty?
|
168
|
+
key.each do |k|
|
169
|
+
new_key = k.to_file(@key_dir_path)
|
170
|
+
index.add new_key
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
# Write index to git and resync fs
|
176
|
+
commit_tree = index.write_tree @repo
|
177
|
+
index.write
|
178
|
+
|
179
|
+
commit_author = { email: 'wee@example.org', name: 'gitolite-rugged gem', time: Time.now }
|
180
|
+
|
181
|
+
Rugged::Commit.create(@repo,
|
182
|
+
author: commit_author,
|
183
|
+
committer: commit_author,
|
184
|
+
message: @settings[:commit_msg],
|
185
|
+
parents: [repo.head.target],
|
186
|
+
tree: commit_tree,
|
187
|
+
update_ref: 'HEAD'
|
188
|
+
)
|
189
|
+
end
|
190
|
+
|
191
|
+
|
192
|
+
# Push back to origin
|
193
|
+
def apply
|
194
|
+
@repo.push 'origin', ['refs/heads/master']
|
195
|
+
end
|
196
|
+
|
197
|
+
|
198
|
+
# Commits all staged changes and pushes back to origin
|
199
|
+
def save_and_apply()
|
200
|
+
save
|
201
|
+
apply
|
202
|
+
end
|
203
|
+
|
204
|
+
|
205
|
+
# Updates the repo with changes from remote master
|
206
|
+
# Warning: This resets the repo before pulling in the changes.
|
207
|
+
def update(settings = {})
|
208
|
+
reset!
|
209
|
+
|
210
|
+
# Currently, this only supports merging origin/master into master.
|
211
|
+
master = repo.branches["master"].target
|
212
|
+
origin_master = repo.branches["origin/master"].target
|
213
|
+
|
214
|
+
# Create the merged index in memory
|
215
|
+
merge_index = repo.merge_commits(master, origin_master)
|
216
|
+
|
217
|
+
# Complete the merge by comitting it
|
218
|
+
merge_commit = Rugged::Commit.create(@repo,
|
219
|
+
parents: [ master, origin_master ],
|
220
|
+
tree: merge_index.write_tree(@repo),
|
221
|
+
message: '[gitolite-rugged] Merged `origin/master` into `master`',
|
222
|
+
author: @commit_author,
|
223
|
+
committer: @commit_author,
|
224
|
+
update_ref: 'master'
|
225
|
+
)
|
226
|
+
|
227
|
+
reload!
|
228
|
+
end
|
229
|
+
|
230
|
+
|
231
|
+
private
|
232
|
+
|
233
|
+
|
234
|
+
# Clone the gitolite-admin repo
|
235
|
+
# to the given path.
|
236
|
+
#
|
237
|
+
# The repo is cloned from the url
|
238
|
+
# +(:git_user)@(:hostname)/gitolite-admin.git+
|
239
|
+
#
|
240
|
+
# The hostname may use an optional :port to allow for custom SSH ports.
|
241
|
+
# E.g., +git@localhost:2222/gitolite-admin.git+
|
242
|
+
#
|
243
|
+
def clone()
|
244
|
+
Rugged::Repository.clone_at(admin_url(@settings), File.expand_path(@path), credentials: @creds)
|
245
|
+
end
|
246
|
+
|
247
|
+
|
248
|
+
def load_config
|
249
|
+
Config.new(@config_file_path)
|
250
|
+
end
|
251
|
+
|
252
|
+
|
253
|
+
def list_keys
|
254
|
+
Dir.glob(@key_dir_path + '/**/*.pub')
|
255
|
+
end
|
256
|
+
|
257
|
+
|
258
|
+
# Loads all .pub files in the gitolite-admin
|
259
|
+
# keydir directory
|
260
|
+
def load_keys
|
261
|
+
keys = Hash.new {|k,v| k[v] = DirtyProxy.new([])}
|
262
|
+
|
263
|
+
list_keys.each do |key|
|
264
|
+
new_key = SSHKey.from_file(key)
|
265
|
+
owner = new_key.owner
|
266
|
+
|
267
|
+
keys[owner] << new_key
|
268
|
+
end
|
269
|
+
|
270
|
+
# Mark key sets as unmodified (for dirty checking)
|
271
|
+
keys.values.each{|set| set.clean_up!}
|
272
|
+
|
273
|
+
keys
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module Gitolite
|
2
|
+
|
3
|
+
# Models an SSH key within gitolite
|
4
|
+
# provides support for multikeys
|
5
|
+
#
|
6
|
+
# Types of multi keys:
|
7
|
+
# bob.pub => username: bob
|
8
|
+
# bob@desktop.pub => username: bob, location: desktop
|
9
|
+
# bob@email.com.pub => username: bob@email.com
|
10
|
+
# bob@email.com@desktop.pub => username: bob@email.com, location: desktop
|
11
|
+
|
12
|
+
class SSHKey
|
13
|
+
|
14
|
+
attr_accessor :owner, :location, :type, :blob, :email
|
15
|
+
|
16
|
+
class << self
|
17
|
+
|
18
|
+
def from_file(key)
|
19
|
+
raise "#{key} does not exist!" unless File.exists?(key)
|
20
|
+
|
21
|
+
# TODO this is old-style locations, use folders instead.
|
22
|
+
# Get our owner and location
|
23
|
+
File.basename(key) =~ /^([\+\w\.-]+(?:@(?:[\w-]+\.)+\D{2,4})?)(?:@([\w-]+))?.pub$/i
|
24
|
+
owner = $1
|
25
|
+
location = $2 || ""
|
26
|
+
|
27
|
+
# Use string key constructor
|
28
|
+
self.from_string(File.read(key), owner, location)
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
# Construct a SSHKey from a string
|
33
|
+
def from_string(key_string, owner, location = "")
|
34
|
+
if owner.nil?
|
35
|
+
raise ArgumentError, "owner was nil, you must specify an owner"
|
36
|
+
end
|
37
|
+
|
38
|
+
# Get parts of the key
|
39
|
+
type, blob, email = key_string.split
|
40
|
+
|
41
|
+
# We need at least a type or blob
|
42
|
+
if type.nil? || blob.nil?
|
43
|
+
raise ArgumentError, "'#{key_string}' is not a valid SSH key string"
|
44
|
+
end
|
45
|
+
|
46
|
+
# If the key didn't have an email, just use the owner
|
47
|
+
if email.nil?
|
48
|
+
email = owner
|
49
|
+
end
|
50
|
+
|
51
|
+
self.new(type, blob, email, owner, location)
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
def initialize(type, blob, email, owner = nil, location = "")
|
58
|
+
@type = type
|
59
|
+
@blob = blob
|
60
|
+
@email = email
|
61
|
+
|
62
|
+
@owner = owner || email
|
63
|
+
@location = location
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
def to_s
|
68
|
+
[@type, @blob, @email].join(' ')
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
def to_file(path)
|
73
|
+
key_file = File.join(path, self.filename)
|
74
|
+
File.open(key_file, "w") do |f|
|
75
|
+
f.sync = true
|
76
|
+
f.write(self.to_s)
|
77
|
+
end
|
78
|
+
key_file
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
def filename
|
83
|
+
file = @owner
|
84
|
+
file += "@#{@location}" unless @location.empty?
|
85
|
+
file += ".pub"
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
def ==(key)
|
90
|
+
@type == key.type &&
|
91
|
+
@blob == key.blob &&
|
92
|
+
@email == key.email &&
|
93
|
+
@owner == key.owner &&
|
94
|
+
@location == key.location
|
95
|
+
end
|
96
|
+
|
97
|
+
|
98
|
+
def hash
|
99
|
+
[@owner, @location, @type, @blob, @email].hash
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
end
|
data/lib/gitolite.rb
ADDED