gitolite 0.0.3.alpha → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile.lock +30 -20
- data/README.markdown +179 -0
- data/gitolite.gemspec +6 -5
- data/lib/gitolite.rb +1 -0
- data/lib/gitolite/config.rb +34 -3
- data/lib/gitolite/config/repo.rb +1 -3
- data/lib/gitolite/gitolite_admin.rb +45 -5
- data/lib/gitolite/ssh_key.rb +16 -2
- data/lib/gitolite/version.rb +1 -1
- data/spec/config_spec.rb +64 -0
- data/spec/repo_spec.rb +87 -0
- data/spec/ssh_key_spec.rb +94 -34
- metadata +114 -116
- data/README.rdoc +0 -144
data/Gemfile.lock
CHANGED
@@ -1,30 +1,40 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
gitolite (0.0.
|
5
|
-
grit (~> 2.
|
6
|
-
hashery (~> 1.
|
4
|
+
gitolite (0.0.4.alpha)
|
5
|
+
grit (~> 2.5.0)
|
6
|
+
hashery (~> 1.5.0)
|
7
7
|
|
8
8
|
GEM
|
9
9
|
remote: http://rubygems.org/
|
10
10
|
specs:
|
11
|
-
|
11
|
+
blankslate (2.1.2.4)
|
12
|
+
diff-lcs (1.1.3)
|
12
13
|
forgery (0.5.0)
|
13
|
-
grit (2.
|
14
|
+
grit (2.5.0)
|
14
15
|
diff-lcs (~> 1.1)
|
15
16
|
mime-types (~> 1.15)
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
rspec
|
26
|
-
|
27
|
-
|
17
|
+
posix-spawn (~> 0.3.6)
|
18
|
+
hashery (1.5.0)
|
19
|
+
blankslate
|
20
|
+
json (1.6.6)
|
21
|
+
mime-types (1.18)
|
22
|
+
multi_json (1.3.2)
|
23
|
+
posix-spawn (0.3.6)
|
24
|
+
rdoc (3.12)
|
25
|
+
json (~> 1.4)
|
26
|
+
rspec (2.9.0)
|
27
|
+
rspec-core (~> 2.9.0)
|
28
|
+
rspec-expectations (~> 2.9.0)
|
29
|
+
rspec-mocks (~> 2.9.0)
|
30
|
+
rspec-core (2.9.0)
|
31
|
+
rspec-expectations (2.9.1)
|
32
|
+
diff-lcs (~> 1.1.3)
|
33
|
+
rspec-mocks (2.9.0)
|
34
|
+
simplecov (0.6.2)
|
35
|
+
multi_json (~> 1.3)
|
36
|
+
simplecov-html (~> 0.5.3)
|
37
|
+
simplecov-html (0.5.3)
|
28
38
|
|
29
39
|
PLATFORMS
|
30
40
|
ruby
|
@@ -32,6 +42,6 @@ PLATFORMS
|
|
32
42
|
DEPENDENCIES
|
33
43
|
forgery (~> 0.5.0)
|
34
44
|
gitolite!
|
35
|
-
|
36
|
-
|
37
|
-
|
45
|
+
rdoc (~> 3.12)
|
46
|
+
rspec (~> 2.9.0)
|
47
|
+
simplecov (~> 0.6.2)
|
data/README.markdown
ADDED
@@ -0,0 +1,179 @@
|
|
1
|
+
# gitolite #
|
2
|
+
|
3
|
+
This gem is designed to provide a Ruby interface to the {gitolite}[https://github.com/sitaramc/gitolite] git backend system. I am aiming to provide all management functionality that is available via the gitolite-admin repository (like SSH keys, repository permissions, etc)
|
4
|
+
|
5
|
+
This gem can still have problems. Please file an issue if you encounter a bug. If you have a feature request, file one please.
|
6
|
+
|
7
|
+
## Features ##
|
8
|
+
* Allows for the creation and management of repos within gitolite
|
9
|
+
* Allows for the creation and deletion of SSH keys within gitolite
|
10
|
+
* Allows for the bootstrapping of a gitolite-admin repository
|
11
|
+
|
12
|
+
## Issues ##
|
13
|
+
* Gem is not thread safe. For now, the gem will change directories in order to perform git operations. It will, however, return to the old working directory once it is finished. I am looking into making the gem thread safe.
|
14
|
+
|
15
|
+
## Requirements ##
|
16
|
+
* Ruby 1.8.x or 1.9.x
|
17
|
+
* a working {gitolite}[https://github.com/sitaramc/gitolite] installation
|
18
|
+
* the <tt>gitolite-admin</tt> repository checked out locally
|
19
|
+
|
20
|
+
## Installation ##
|
21
|
+
|
22
|
+
gem install gitolite
|
23
|
+
|
24
|
+
## Usage ##
|
25
|
+
|
26
|
+
### Load a gitolite-admin repo ###
|
27
|
+
|
28
|
+
require 'gitolite'
|
29
|
+
ga_repo = Gitolite::GitoliteAdmin.new("/path/to/gitolite/admin/repo")
|
30
|
+
|
31
|
+
This method can only be called on an existing gitolite-admin repo. If you need to create a new gitolite-admin repo, see "Bootstrapping".
|
32
|
+
|
33
|
+
### Configuration Files ###
|
34
|
+
|
35
|
+
conf = ga_repo.config
|
36
|
+
|
37
|
+
#Empty configs can also be initialized
|
38
|
+
conf2 = Config.init # => defaults to a filename of gitolite.conf
|
39
|
+
conf2 = Config.init("new_config.conf")
|
40
|
+
|
41
|
+
#Filename is set to whatever the filename was when the config was created
|
42
|
+
conf.filename # => "gitolite.conf"
|
43
|
+
conf2.filename # => "new_config.conf")
|
44
|
+
|
45
|
+
#filename can be changed via the setter
|
46
|
+
conf2.filename = "new_config.conf"
|
47
|
+
|
48
|
+
#to_file will write the config out to the file system
|
49
|
+
#using the value of the filename attribute. An alternative
|
50
|
+
#filename can also be specified
|
51
|
+
conf.to_file("/new/config/path") # => writes /new/config/path/gitolite.conf
|
52
|
+
conf.to_file("/new/config/path", "test.conf") # => writes /new/config/path/test.conf
|
53
|
+
|
54
|
+
### Repo management ###
|
55
|
+
|
56
|
+
repo = Gitolite::Config::Repo.new("AwesomeRepo")
|
57
|
+
|
58
|
+
#For a list of permissions, see http://sitaramc.github.com/gitolite/conf.html#gitolite
|
59
|
+
repo.add_permission("RW+", "", "bob", "joe", "susan")
|
60
|
+
|
61
|
+
#Add repo to config
|
62
|
+
conf.add_repo(repo)
|
63
|
+
|
64
|
+
#Delete repo by object
|
65
|
+
conf.rm_repo(repo)
|
66
|
+
|
67
|
+
#Delete a repo by name
|
68
|
+
conf.rm_repo("AwesomeRepo")
|
69
|
+
conf.rm_repo(:AwesomeRepo)
|
70
|
+
|
71
|
+
#Test if repo exists by name
|
72
|
+
conf.has_repo?('cool_repo') # => false
|
73
|
+
conf.has_repo?(:cool_repo) # => false
|
74
|
+
|
75
|
+
#Can also pass a Gitolite::Config::Repo object
|
76
|
+
repo = Gitolite::Config::Repo.new('cool_repo')
|
77
|
+
conf.has_repo?(repo) # => true
|
78
|
+
|
79
|
+
#Get a repo object from the config
|
80
|
+
repo = conf.get_repo('cool_repo')
|
81
|
+
repo = conf.get_repo(:cool_repo)
|
82
|
+
|
83
|
+
### SSH Key Management ###
|
84
|
+
|
85
|
+
#Three ways to create keys: manually, from an existing key, or from a string representing a key
|
86
|
+
key = Gitolite::SSHKey.new("ssh-rsa", "big-public-key-blob", "email")
|
87
|
+
key2 = Gitolite::SSHKey.from_file("/path/to/ssh/key.pub")
|
88
|
+
|
89
|
+
key_string = File.read("/path/to/ssh/key.pub")
|
90
|
+
key3 = Gitolite::SSHKey.from_string(key_string, "owner")
|
91
|
+
|
92
|
+
|
93
|
+
#Add the keys
|
94
|
+
ga_repo.add_key(key)
|
95
|
+
ga_repo.add_key(key2)
|
96
|
+
ga_repo.add_key(key3)
|
97
|
+
|
98
|
+
#Remove key2
|
99
|
+
ga_repo.rm_key(key2)
|
100
|
+
|
101
|
+
### Save changes ###
|
102
|
+
|
103
|
+
ga_repo.save
|
104
|
+
|
105
|
+
When this method is called, all changes get written to the file system and staged in git. For the time being, gitolite assumes full control of the gitolite-admin repository. This means that any keys in the keydir that are not being tracked will be removed and any human changes to gitolite.conf will be erased.
|
106
|
+
|
107
|
+
### Apply changes ###
|
108
|
+
ga_repo.apply
|
109
|
+
|
110
|
+
This method will commit all changes with a generic message (will be improved upon later) and push to <tt>origin master</tt>.
|
111
|
+
|
112
|
+
### Save and apply ###
|
113
|
+
ga_repo.save_and_apply
|
114
|
+
|
115
|
+
### Updating remote changes ###
|
116
|
+
#In order to avoid conflicts, this will perform a reset! by default
|
117
|
+
#pass :reset => false to disable the reset (Git conflicts will have to be manually fixed)
|
118
|
+
ga_repo.update
|
119
|
+
ga_repo.update(:reset => false)
|
120
|
+
|
121
|
+
#Update while performing a rebase
|
122
|
+
ga_repo.update(:rebase => true)
|
123
|
+
|
124
|
+
### Reloading from the file system ###
|
125
|
+
ga_repo.reload!
|
126
|
+
|
127
|
+
### Resetting to HEAD, destroying all local changes (including untracked files) ###
|
128
|
+
#This will also perform a reload!
|
129
|
+
ga_repo.reset!
|
130
|
+
|
131
|
+
### Bootstrapping ###
|
132
|
+
ga_repo = GitoliteAdmin.bootstrap("/path/to/new/gitolite/repo")
|
133
|
+
|
134
|
+
This will create the folders <tt>conf</tt> and <tt>keydir</tt> in the supplied path. A config file will also be created in the conf directory. The default configuration supplies RW+ permissions to a user named git for a repo named <tt>gitolite-admin</tt>. You can specify an options hash to change some values:
|
135
|
+
|
136
|
+
ga_repo = GitoliteAdmin.bootstrap("/path/to/new/gitolite/repo", {:user => "admin", :perm => "RW"})
|
137
|
+
|
138
|
+
You can also pass a message to be used for the initial bootstrap commit:
|
139
|
+
|
140
|
+
ga_repo = GitoliteAdmin.bootstrap("/path/to/new/gitolite/repo", {:message => "Bootstrapped new repo"})
|
141
|
+
|
142
|
+
Please note that while bootstrapping is supported, I highly recommend that the initial gitolite-admin repo be created by gitolite itself.
|
143
|
+
|
144
|
+
## Caveats ##
|
145
|
+
### Windows compatibility ###
|
146
|
+
The grit gem (which is used for under-the-hood git operations) does not currently support Windows. Until it does, gitolite will be unable to support Windows.
|
147
|
+
|
148
|
+
### Group Ordering ###
|
149
|
+
When the gitolite backend parses the config file, it does so in one pass. Because of this, groups that are modified after being used do not see those changes reflected in previous uses.
|
150
|
+
|
151
|
+
For example:
|
152
|
+
|
153
|
+
@groupa = bob joe sue
|
154
|
+
@groupb = jim @groupa
|
155
|
+
@groupa = sam
|
156
|
+
|
157
|
+
Group b will contain the users <tt>jim, bob, joe, and sue</tt>
|
158
|
+
|
159
|
+
The gitolite gem, on the other hand, will <em>always</em> output groups so that all modifications are represented before it is ever used. For the above example, group b will be output with the following users: <tt>jim, bob, joe, sue, and sam</tt>. The groups in the config file will look like this:
|
160
|
+
|
161
|
+
@groupa = bob joe sue sam
|
162
|
+
@groupb = jim @groupa
|
163
|
+
|
164
|
+
# Contributing #
|
165
|
+
* Tests! If you ask me to pull changes that are not adequately tested, I'm not going to do it.
|
166
|
+
* If you introduce new features/public methods on objects, you must update the README.
|
167
|
+
|
168
|
+
### Contributors ###
|
169
|
+
* Alexander Simonov - [simonoff](https://github.com/simonoff)
|
170
|
+
|
171
|
+
## Documentation ##
|
172
|
+
* Rdoc is coming eventually
|
173
|
+
|
174
|
+
## Future ##
|
175
|
+
* support folders in the keydir
|
176
|
+
* support include tags
|
177
|
+
* cleanup methods to make adding and removing easier (like add_key should accept an array of keys)
|
178
|
+
* Make the gem thread safe
|
179
|
+
* Rails integration via [gitolite-rails](https://www.github.com/wingrunr21/gitolite-rails)
|
data/gitolite.gemspec
CHANGED
@@ -14,12 +14,13 @@ Gem::Specification.new do |s|
|
|
14
14
|
|
15
15
|
s.rubyforge_project = "gitolite"
|
16
16
|
|
17
|
-
s.add_development_dependency "rspec", "~> 2.
|
17
|
+
s.add_development_dependency "rspec", "~> 2.9.0"
|
18
18
|
s.add_development_dependency "forgery", "~> 0.5.0"
|
19
|
-
s.add_development_dependency "rdoc", "~> 3.
|
20
|
-
s.add_development_dependency "
|
21
|
-
s.add_dependency "grit", "~> 2.
|
22
|
-
s.add_dependency "hashery", "~> 1.
|
19
|
+
s.add_development_dependency "rdoc", "~> 3.12"
|
20
|
+
s.add_development_dependency "simplecov", "~> 0.6.2"
|
21
|
+
s.add_dependency "grit", "~> 2.5.0"
|
22
|
+
s.add_dependency "hashery", "~> 1.5.0"
|
23
|
+
s.add_dependency "plexus", "~> 0.5.10"
|
23
24
|
|
24
25
|
s.files = `git ls-files`.split("\n")
|
25
26
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
data/lib/gitolite.rb
CHANGED
data/lib/gitolite/config.rb
CHANGED
@@ -68,7 +68,8 @@ module Gitolite
|
|
68
68
|
new_conf = File.join(path, filename)
|
69
69
|
File.open(new_conf, "w") do |f|
|
70
70
|
#Output groups
|
71
|
-
|
71
|
+
dep_order = build_groups_depgraph
|
72
|
+
dep_order.each {|group| f.write group.to_s }
|
72
73
|
|
73
74
|
gitweb_descs = []
|
74
75
|
@repos.each do |k, v|
|
@@ -106,7 +107,7 @@ module Gitolite
|
|
106
107
|
line = cleanup_config_line(l)
|
107
108
|
next if line.empty? #lines are empty if we killed a comment
|
108
109
|
|
109
|
-
case line
|
110
|
+
case line
|
110
111
|
#found a repo definition
|
111
112
|
when /^repo (.*)/
|
112
113
|
#Empty our current context
|
@@ -119,7 +120,7 @@ module Gitolite
|
|
119
120
|
@repos[r] = Repo.new(r) unless has_repo?(r)
|
120
121
|
end
|
121
122
|
#repo permissions
|
122
|
-
when /^(-|C|R|RW\+?(?:C?D?|D?C?)) (.* )?= (.+)/
|
123
|
+
when /^(-|C|R|RW\+?(?:C?D?|D?C?)M?) (.* )?= (.+)/
|
123
124
|
perm = $1
|
124
125
|
refex = $2 || ""
|
125
126
|
users = $3.split
|
@@ -199,8 +200,38 @@ module Gitolite
|
|
199
200
|
end
|
200
201
|
end
|
201
202
|
|
203
|
+
# Builds a dependency tree from the groups in order to ensure all groups
|
204
|
+
# are defined before they are used
|
205
|
+
def build_groups_depgraph
|
206
|
+
dp = ::Plexus::Digraph.new
|
207
|
+
|
208
|
+
# Add each group to the graph
|
209
|
+
@groups.each_value do |group|
|
210
|
+
# Select group names from the users
|
211
|
+
subgroups = group.users.select {|u| u =~ /^#{Group::PREPEND_CHAR}.*$/}
|
212
|
+
.map{|g| get_group g.gsub(Group::PREPEND_CHAR, '') }
|
213
|
+
|
214
|
+
subgroups.each do |subgroup|
|
215
|
+
dp.add_edge! subgroup, group
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
# Figure out if we have a good depedency graph
|
220
|
+
dep_order = dp.topsort
|
221
|
+
|
222
|
+
if dep_order.empty?
|
223
|
+
raise GroupDependencyError unless @groups.empty?
|
224
|
+
end
|
225
|
+
|
226
|
+
dep_order
|
227
|
+
end
|
228
|
+
|
202
229
|
#Raised when something in a config fails to parse properly
|
203
230
|
class ParseError < RuntimeError
|
204
231
|
end
|
232
|
+
|
233
|
+
# Raised when group dependencies cannot be suitably resolved for output
|
234
|
+
class GroupDependencyError < RuntimeError
|
235
|
+
end
|
205
236
|
end
|
206
237
|
end
|
data/lib/gitolite/config/repo.rb
CHANGED
@@ -1,11 +1,9 @@
|
|
1
|
-
require 'hashery'
|
2
|
-
|
3
1
|
module Gitolite
|
4
2
|
class Config
|
5
3
|
#Represents a repo inside the gitolite configuration. The name, permissions, and git config
|
6
4
|
#options are all encapsulated in this class
|
7
5
|
class Repo
|
8
|
-
ALLOWED_PERMISSIONS = /-|R|RW
|
6
|
+
ALLOWED_PERMISSIONS = /-|C|R|RW\+?(?:C?D?|D?C?)M?/
|
9
7
|
|
10
8
|
attr_accessor :permissions, :name, :config, :owner, :description
|
11
9
|
|
@@ -6,17 +6,21 @@ module Gitolite
|
|
6
6
|
CONFDIR = "conf"
|
7
7
|
KEYDIR = "keydir"
|
8
8
|
|
9
|
+
#Gitolite gem's default git commit message
|
10
|
+
DEFAULT_COMMIT_MSG = "Committed by the gitolite gem"
|
11
|
+
|
9
12
|
# Intialize with the path to
|
10
13
|
# the gitolite-admin repository
|
11
14
|
def initialize(path, options = {})
|
15
|
+
@path = path
|
12
16
|
@gl_admin = Grit::Repo.new(path)
|
13
17
|
|
14
18
|
@conf = options[:conf] || CONF
|
15
19
|
@confdir = options[:confdir] || CONFDIR
|
16
20
|
@keydir = options[:keydir] || KEYDIR
|
17
21
|
|
18
|
-
|
19
|
-
|
22
|
+
# Load the ssh keys and the configuration
|
23
|
+
load_data
|
20
24
|
end
|
21
25
|
|
22
26
|
# This method will bootstrap a gitolite-admin repo
|
@@ -80,20 +84,50 @@ module Gitolite
|
|
80
84
|
end
|
81
85
|
end
|
82
86
|
|
87
|
+
# This method will destroy all local tracked changes, resetting the local gitolite
|
88
|
+
# git repo to HEAD and reloading the entire repository
|
89
|
+
# Note that this will also delete all untracked files
|
90
|
+
def reset!
|
91
|
+
Dir.chdir(@gl_admin.working_dir) do
|
92
|
+
@gl_admin.git.reset({:hard => true}, 'HEAD')
|
93
|
+
@gl_admin.git.clean({:d => true, :q => true, :f => true})
|
94
|
+
end
|
95
|
+
reload!
|
96
|
+
end
|
97
|
+
|
98
|
+
# This method will destroy the in-memory data structures and reload everything
|
99
|
+
# from the file system
|
100
|
+
def reload!
|
101
|
+
load_data
|
102
|
+
end
|
103
|
+
|
83
104
|
#commits all staged changes and pushes back
|
84
105
|
#to origin
|
85
106
|
#
|
86
107
|
#TODO: generate a better commit message
|
87
108
|
#TODO: add the ability to specify the remote and branch
|
88
109
|
#TODO: detect existance of origin instead of just dying
|
89
|
-
def apply(commit_message =
|
110
|
+
def apply(commit_message = DEFAULT_COMMIT_MSG)
|
90
111
|
@gl_admin.commit_index(commit_message)
|
91
112
|
@gl_admin.git.push({}, "origin", "master")
|
92
113
|
end
|
93
114
|
|
94
|
-
def save_and_apply
|
115
|
+
def save_and_apply(commit_message = DEFAULT_COMMIT_MSG)
|
95
116
|
self.save
|
96
|
-
self.apply
|
117
|
+
self.apply(commit_message)
|
118
|
+
end
|
119
|
+
|
120
|
+
# Updates the repo with changes from remote master
|
121
|
+
def update(options = {})
|
122
|
+
options = {:reset => true, :rebase => false }.merge(options)
|
123
|
+
|
124
|
+
reset! if options[:reset]
|
125
|
+
|
126
|
+
Dir.chdir(@gl_admin.working_dir) do
|
127
|
+
@gl_admin.git.pull({:rebase => options[:rebase]}, "origin", "master")
|
128
|
+
end
|
129
|
+
|
130
|
+
reload!
|
97
131
|
end
|
98
132
|
|
99
133
|
def add_key(key)
|
@@ -102,6 +136,7 @@ module Gitolite
|
|
102
136
|
end
|
103
137
|
|
104
138
|
def rm_key(key)
|
139
|
+
raise "Key must be of type Gitolite::SSHKey!" unless key.instance_of? Gitolite::SSHKey
|
105
140
|
@ssh_keys[key.owner].delete key
|
106
141
|
end
|
107
142
|
|
@@ -124,6 +159,11 @@ module Gitolite
|
|
124
159
|
end
|
125
160
|
|
126
161
|
private
|
162
|
+
def load_data
|
163
|
+
@ssh_keys = load_keys(File.join(@path, @keydir))
|
164
|
+
@config = Config.new(File.join(@path, @confdir, @conf))
|
165
|
+
end
|
166
|
+
|
127
167
|
#Loads all .pub files in the gitolite-admin
|
128
168
|
#keydir directory
|
129
169
|
def load_keys(path)
|