gitolite-rugged 1.2.pre.devel
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 +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