jbox-gitolite 1.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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