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.
@@ -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