steveh-grit 1.1.1

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.
data/lib/grit/repo.rb ADDED
@@ -0,0 +1,437 @@
1
+ module Grit
2
+
3
+ class Repo
4
+ DAEMON_EXPORT_FILE = 'git-daemon-export-ok'
5
+
6
+ # The path of the git repo as a String
7
+ attr_accessor :path
8
+ attr_accessor :working_dir
9
+ attr_reader :bare
10
+
11
+ # The git command line interface object
12
+ attr_accessor :git
13
+
14
+ # Create a new Repo instance
15
+ # +path+ is the path to either the root git directory or the bare git repo
16
+ # +options+ :is_bare force to load a bare repo
17
+ #
18
+ # Examples
19
+ # g = Repo.new("/Users/tom/dev/grit")
20
+ # g = Repo.new("/Users/tom/public/grit.git")
21
+ #
22
+ # Returns Grit::Repo
23
+ def initialize(path, options = {})
24
+ epath = File.expand_path(path)
25
+
26
+ if File.exist?(File.join(epath, '.git'))
27
+ self.working_dir = epath
28
+ self.path = File.join(epath, '.git')
29
+ @bare = false
30
+ elsif File.exist?(epath) && (epath =~ /\.git$/ || options[:is_bare])
31
+ self.path = epath
32
+ @bare = true
33
+ elsif File.exist?(epath)
34
+ raise InvalidGitRepositoryError.new(epath)
35
+ else
36
+ raise NoSuchPathError.new(epath)
37
+ end
38
+
39
+ self.git = Git.new(self.path, self.working_dir)
40
+ end
41
+
42
+ # Does nothing yet...
43
+ def self.init(path)
44
+ # !! TODO !!
45
+ # create directory
46
+ # generate initial git directory
47
+ # create new Grit::Repo on that dir, return it
48
+ end
49
+
50
+ # The project's description. Taken verbatim from GIT_REPO/description
51
+ #
52
+ # Returns String
53
+ def description
54
+ File.open(File.join(self.path, 'description')).read.chomp
55
+ end
56
+
57
+ def blame(file, commit = nil)
58
+ Blame.new(self, file, commit)
59
+ end
60
+
61
+
62
+ # An array of Head objects representing the branch heads in
63
+ # this repo
64
+ #
65
+ # Returns Grit::Head[] (baked)
66
+ def heads
67
+ Head.find_all(self)
68
+ end
69
+
70
+ alias_method :branches, :heads
71
+
72
+ def get_head(head_name)
73
+ heads.find { |h| h.name == head_name }
74
+ end
75
+
76
+ def is_head?(head_name)
77
+ get_head(head_name)
78
+ end
79
+
80
+ # Object reprsenting the current repo head.
81
+ #
82
+ # Returns Grit::Head (baked)
83
+ def head
84
+ Head.current(self)
85
+ end
86
+
87
+
88
+ # Commits current index
89
+ #
90
+ # Returns true/false if commit worked
91
+ def commit_index(message)
92
+ self.git.commit({}, '-m', message)
93
+ end
94
+
95
+ # Commits all tracked and modified files
96
+ #
97
+ # Returns true/false if commit worked
98
+ def commit_all(message)
99
+ self.git.commit({}, '-a', '-m', message)
100
+ end
101
+
102
+ # Adds files to the index
103
+ def add(*files)
104
+ self.git.add({}, *files.flatten)
105
+ end
106
+
107
+ # Remove files from the index
108
+ def remove(*files)
109
+ self.git.rm({}, *files.flatten)
110
+ end
111
+
112
+
113
+ def blame_tree(commit, path = nil)
114
+ commit_array = self.git.blame_tree(commit, path)
115
+
116
+ final_array = {}
117
+ commit_array.each do |file, sha|
118
+ final_array[file] = commit(sha)
119
+ end
120
+ final_array
121
+ end
122
+
123
+ def status
124
+ Status.new(self)
125
+ end
126
+
127
+
128
+ # An array of Tag objects that are available in this repo
129
+ #
130
+ # Returns Grit::Tag[] (baked)
131
+ def tags
132
+ Tag.find_all(self)
133
+ end
134
+
135
+ # An array of Remote objects representing the remote branches in
136
+ # this repo
137
+ #
138
+ # Returns Grit::Remote[] (baked)
139
+ def remotes
140
+ Remote.find_all(self)
141
+ end
142
+
143
+ # An array of Ref objects representing the refs in
144
+ # this repo
145
+ #
146
+ # Returns Grit::Ref[] (baked)
147
+ def refs
148
+ [ Head.find_all(self), Tag.find_all(self), Remote.find_all(self) ].flatten
149
+ end
150
+
151
+ def commit_stats(start = 'master', max_count = 10, skip = 0)
152
+ options = {:max_count => max_count,
153
+ :skip => skip}
154
+
155
+ CommitStats.find_all(self, start, options)
156
+ end
157
+
158
+ # An array of Commit objects representing the history of a given ref/commit
159
+ # +start+ is the branch/commit name (default 'master')
160
+ # +max_count+ is the maximum number of commits to return (default 10, use +false+ for all)
161
+ # +skip+ is the number of commits to skip (default 0)
162
+ #
163
+ # Returns Grit::Commit[] (baked)
164
+ def commits(start = 'master', max_count = 10, skip = 0)
165
+ options = {:max_count => max_count,
166
+ :skip => skip}
167
+
168
+ Commit.find_all(self, start, options)
169
+ end
170
+
171
+ # The Commits objects that are reachable via +to+ but not via +from+
172
+ # Commits are returned in chronological order.
173
+ # +from+ is the branch/commit name of the younger item
174
+ # +to+ is the branch/commit name of the older item
175
+ #
176
+ # Returns Grit::Commit[] (baked)
177
+ def commits_between(from, to)
178
+ Commit.find_all(self, "#{from}..#{to}").reverse
179
+ end
180
+
181
+ # The Commits objects that are newer than the specified date.
182
+ # Commits are returned in chronological order.
183
+ # +start+ is the branch/commit name (default 'master')
184
+ # +since+ is a string represeting a date/time
185
+ # +extra_options+ is a hash of extra options
186
+ #
187
+ # Returns Grit::Commit[] (baked)
188
+ def commits_since(start = 'master', since = '1970-01-01', extra_options = {})
189
+ options = {:since => since}.merge(extra_options)
190
+
191
+ Commit.find_all(self, start, options)
192
+ end
193
+
194
+ # The number of commits reachable by the given branch/commit
195
+ # +start+ is the branch/commit name (default 'master')
196
+ #
197
+ # Returns Integer
198
+ def commit_count(start = 'master')
199
+ Commit.count(self, start)
200
+ end
201
+
202
+ # The Commit object for the specified id
203
+ # +id+ is the SHA1 identifier of the commit
204
+ #
205
+ # Returns Grit::Commit (baked)
206
+ def commit(id)
207
+ options = {:max_count => 1}
208
+
209
+ Commit.find_all(self, id, options).first
210
+ end
211
+
212
+ # Returns a list of commits that is in +other_repo+ but not in self
213
+ #
214
+ # Returns Grit::Commit[]
215
+ def commit_deltas_from(other_repo, ref = "master", other_ref = "master")
216
+ # TODO: we should be able to figure out the branch point, rather than
217
+ # rev-list'ing the whole thing
218
+ repo_refs = self.git.rev_list({}, ref).strip.split("\n")
219
+ other_repo_refs = other_repo.git.rev_list({}, other_ref).strip.split("\n")
220
+
221
+ (other_repo_refs - repo_refs).map do |ref|
222
+ Commit.find_all(other_repo, ref, {:max_count => 1}).first
223
+ end
224
+ end
225
+
226
+ # The Tree object for the given treeish reference
227
+ # +treeish+ is the reference (default 'master')
228
+ # +paths+ is an optional Array of directory paths to restrict the tree (deafult [])
229
+ #
230
+ # Examples
231
+ # repo.tree('master', ['lib/'])
232
+ #
233
+ # Returns Grit::Tree (baked)
234
+ def tree(treeish = 'master', paths = [])
235
+ Tree.construct(self, treeish, paths)
236
+ end
237
+
238
+ # The Blob object for the given id
239
+ # +id+ is the SHA1 id of the blob
240
+ #
241
+ # Returns Grit::Blob (unbaked)
242
+ def blob(id)
243
+ Blob.create(self, :id => id)
244
+ end
245
+
246
+ # The commit log for a treeish
247
+ #
248
+ # Returns Grit::Commit[]
249
+ def log(commit = 'master', path = nil, options = {})
250
+ default_options = {:pretty => "raw"}
251
+ actual_options = default_options.merge(options)
252
+ arg = path ? [commit, '--', path] : [commit]
253
+ commits = self.git.log(actual_options, *arg)
254
+ Commit.list_from_string(self, commits)
255
+ end
256
+
257
+ # The diff from commit +a+ to commit +b+, optionally restricted to the given file(s)
258
+ # +a+ is the base commit
259
+ # +b+ is the other commit
260
+ # +paths+ is an optional list of file paths on which to restrict the diff
261
+ def diff(a, b, *paths)
262
+ self.git.diff({}, a, b, '--', *paths)
263
+ end
264
+
265
+ # The commit diff for the given commit
266
+ # +commit+ is the commit name/id
267
+ #
268
+ # Returns Grit::Diff[]
269
+ def commit_diff(commit)
270
+ Commit.diff(self, commit)
271
+ end
272
+
273
+ # Initialize a bare git repository at the given path
274
+ # +path+ is the full path to the repo (traditionally ends with /<name>.git)
275
+ # +options+ is any additional options to the git init command
276
+ #
277
+ # Examples
278
+ # Grit::Repo.init_bare('/var/git/myrepo.git')
279
+ #
280
+ # Returns Grit::Repo (the newly created repo)
281
+ def self.init_bare(path, git_options = {}, repo_options = {})
282
+ git = Git.new(path)
283
+ git.init(git_options)
284
+ self.new(path, repo_options)
285
+ end
286
+
287
+ # Fork a bare git repository from this repo
288
+ # +path+ is the full path of the new repo (traditionally ends with /<name>.git)
289
+ # +options+ is any additional options to the git clone command (:bare and :shared are true by default)
290
+ #
291
+ # Returns Grit::Repo (the newly forked repo)
292
+ def fork_bare(path, options = {})
293
+ default_options = {:bare => true, :shared => true}
294
+ real_options = default_options.merge(options)
295
+ self.git.clone(real_options, self.path, path)
296
+ Repo.new(path)
297
+ end
298
+
299
+ # Archive the given treeish
300
+ # +treeish+ is the treeish name/id (default 'master')
301
+ # +prefix+ is the optional prefix
302
+ #
303
+ # Examples
304
+ # repo.archive_tar
305
+ # # => <String containing tar archive>
306
+ #
307
+ # repo.archive_tar('a87ff14')
308
+ # # => <String containing tar archive for commit a87ff14>
309
+ #
310
+ # repo.archive_tar('master', 'myproject/')
311
+ # # => <String containing tar archive and prefixed with 'myproject/'>
312
+ #
313
+ # Returns String (containing tar archive)
314
+ def archive_tar(treeish = 'master', prefix = nil)
315
+ options = {}
316
+ options[:prefix] = prefix if prefix
317
+ self.git.archive(options, treeish)
318
+ end
319
+
320
+ # Archive and gzip the given treeish
321
+ # +treeish+ is the treeish name/id (default 'master')
322
+ # +prefix+ is the optional prefix
323
+ #
324
+ # Examples
325
+ # repo.archive_tar_gz
326
+ # # => <String containing tar.gz archive>
327
+ #
328
+ # repo.archive_tar_gz('a87ff14')
329
+ # # => <String containing tar.gz archive for commit a87ff14>
330
+ #
331
+ # repo.archive_tar_gz('master', 'myproject/')
332
+ # # => <String containing tar.gz archive and prefixed with 'myproject/'>
333
+ #
334
+ # Returns String (containing tar.gz archive)
335
+ def archive_tar_gz(treeish = 'master', prefix = nil)
336
+ options = {}
337
+ options[:prefix] = prefix if prefix
338
+ self.git.archive(options, treeish, "| gzip")
339
+ end
340
+
341
+ # Write an archive directly to a file
342
+ # +treeish+ is the treeish name/id (default 'master')
343
+ # +prefix+ is the optional prefix (default nil)
344
+ # +filename+ is the name of the file (default 'archive.tar.gz')
345
+ # +format+ is the optional format (default nil)
346
+ # +pipe+ is the command to run the output through (default 'gzip')
347
+ #
348
+ # Returns nothing
349
+ def archive_to_file(treeish = 'master', prefix = nil, filename = 'archive.tar.gz', format = nil, pipe = "gzip")
350
+ options = {}
351
+ options[:prefix] = prefix if prefix
352
+ options[:format] = format if format
353
+ self.git.archive(options, treeish, "| #{pipe} > #{filename}")
354
+ end
355
+
356
+ # Enable git-daemon serving of this repository by writing the
357
+ # git-daemon-export-ok file to its git directory
358
+ #
359
+ # Returns nothing
360
+ def enable_daemon_serve
361
+ FileUtils.touch(File.join(self.path, DAEMON_EXPORT_FILE))
362
+ end
363
+
364
+ # Disable git-daemon serving of this repository by ensuring there is no
365
+ # git-daemon-export-ok file in its git directory
366
+ #
367
+ # Returns nothing
368
+ def disable_daemon_serve
369
+ FileUtils.rm_f(File.join(self.path, DAEMON_EXPORT_FILE))
370
+ end
371
+
372
+ def gc_auto
373
+ self.git.gc({:auto => true})
374
+ end
375
+
376
+ # The list of alternates for this repo
377
+ #
378
+ # Returns Array[String] (pathnames of alternates)
379
+ def alternates
380
+ alternates_path = File.join(self.path, *%w{objects info alternates})
381
+
382
+ if File.exist?(alternates_path)
383
+ File.read(alternates_path).strip.split("\n")
384
+ else
385
+ []
386
+ end
387
+ end
388
+
389
+ # Sets the alternates
390
+ # +alts+ is the Array of String paths representing the alternates
391
+ #
392
+ # Returns nothing
393
+ def alternates=(alts)
394
+ alts.each do |alt|
395
+ unless File.exist?(alt)
396
+ raise "Could not set alternates. Alternate path #{alt} must exist"
397
+ end
398
+ end
399
+
400
+ if alts.empty?
401
+ File.open(File.join(self.path, *%w{objects info alternates}), 'w') do |f|
402
+ f.write ''
403
+ end
404
+ else
405
+ File.open(File.join(self.path, *%w{objects info alternates}), 'w') do |f|
406
+ f.write alts.join("\n")
407
+ end
408
+ end
409
+ end
410
+
411
+ def config
412
+ @config ||= Config.new(self)
413
+ end
414
+
415
+ def index
416
+ Index.new(self)
417
+ end
418
+
419
+ def update_ref(head, commit_sha)
420
+ return nil if !commit_sha || (commit_sha.size != 40)
421
+
422
+ ref_heads = File.join(self.path, 'refs', 'heads')
423
+ FileUtils.mkdir_p(ref_heads)
424
+ File.open(File.join(ref_heads, head), 'w') do |f|
425
+ f.write(commit_sha)
426
+ end
427
+ commit_sha
428
+
429
+ end
430
+
431
+ # Pretty object inspection
432
+ def inspect
433
+ %Q{#<Grit::Repo "#{@path}">}
434
+ end
435
+ end # Repo
436
+
437
+ end # Grit
@@ -0,0 +1,7 @@
1
+ class String
2
+ if ((defined? RUBY_VERSION) && (RUBY_VERSION[0..2] == "1.9"))
3
+ def getord(offset); self[offset].ord; end
4
+ else
5
+ alias :getord :[]
6
+ end
7
+ end
@@ -0,0 +1,151 @@
1
+ module Grit
2
+
3
+ class Status
4
+ include Enumerable
5
+
6
+ @base = nil
7
+ @files = nil
8
+
9
+ def initialize(base)
10
+ @base = base
11
+ construct_status
12
+ end
13
+
14
+ def changed
15
+ @files.select { |k, f| f.type == 'M' }
16
+ end
17
+
18
+ def added
19
+ @files.select { |k, f| f.type == 'A' }
20
+ end
21
+
22
+ def deleted
23
+ @files.select { |k, f| f.type == 'D' }
24
+ end
25
+
26
+ def untracked
27
+ @files.select { |k, f| f.untracked }
28
+ end
29
+
30
+ def pretty
31
+ out = ''
32
+ self.each do |file|
33
+ out << file.path
34
+ out << "\n\tsha(r) " + file.sha_repo.to_s + ' ' + file.mode_repo.to_s
35
+ out << "\n\tsha(i) " + file.sha_index.to_s + ' ' + file.mode_index.to_s
36
+ out << "\n\ttype " + file.type.to_s
37
+ out << "\n\tstage " + file.stage.to_s
38
+ out << "\n\tuntrac " + file.untracked.to_s
39
+ out << "\n"
40
+ end
41
+ out << "\n"
42
+ out
43
+ end
44
+
45
+ # enumerable method
46
+
47
+ def [](file)
48
+ @files[file]
49
+ end
50
+
51
+ def each
52
+ @files.each do |k, file|
53
+ yield file
54
+ end
55
+ end
56
+
57
+ class StatusFile
58
+ attr_accessor :path, :type, :stage, :untracked
59
+ attr_accessor :mode_index, :mode_repo
60
+ attr_accessor :sha_index, :sha_repo
61
+
62
+ @base = nil
63
+
64
+ def initialize(base, hash)
65
+ @base = base
66
+ @path = hash[:path]
67
+ @type = hash[:type]
68
+ @stage = hash[:stage]
69
+ @mode_index = hash[:mode_index]
70
+ @mode_repo = hash[:mode_repo]
71
+ @sha_index = hash[:sha_index]
72
+ @sha_repo = hash[:sha_repo]
73
+ @untracked = hash[:untracked]
74
+ end
75
+
76
+ def blob(type = :index)
77
+ if type == :repo
78
+ @base.object(@sha_repo)
79
+ else
80
+ @base.object(@sha_index) rescue @base.object(@sha_repo)
81
+ end
82
+ end
83
+
84
+ end
85
+
86
+ private
87
+
88
+ def construct_status
89
+ @files = ls_files
90
+
91
+ Dir.chdir(@base.working_dir) do
92
+ # find untracked in working dir
93
+ Dir.glob('**/*') do |file|
94
+ if !@files[file]
95
+ @files[file] = {:path => file, :untracked => true} if !File.directory?(file)
96
+ end
97
+ end
98
+
99
+ # find modified in tree
100
+ diff_files.each do |path, data|
101
+ @files[path] ? @files[path].merge!(data) : @files[path] = data
102
+ end
103
+
104
+ # find added but not committed - new files
105
+ diff_index('HEAD').each do |path, data|
106
+ @files[path] ? @files[path].merge!(data) : @files[path] = data
107
+ end
108
+
109
+ @files.each do |k, file_hash|
110
+ @files[k] = StatusFile.new(@base, file_hash)
111
+ end
112
+ end
113
+ end
114
+
115
+ # compares the index and the working directory
116
+ def diff_files
117
+ hsh = {}
118
+ @base.git.diff_files.split("\n").each do |line|
119
+ (info, file) = line.split("\t")
120
+ (mode_src, mode_dest, sha_src, sha_dest, type) = info.split
121
+ hsh[file] = {:path => file, :mode_file => mode_src.to_s[1, 7], :mode_index => mode_dest,
122
+ :sha_file => sha_src, :sha_index => sha_dest, :type => type}
123
+ end
124
+ hsh
125
+ end
126
+
127
+ # compares the index and the repository
128
+ def diff_index(treeish)
129
+ hsh = {}
130
+ @base.git.diff_index({}, treeish).split("\n").each do |line|
131
+ (info, file) = line.split("\t")
132
+ (mode_src, mode_dest, sha_src, sha_dest, type) = info.split
133
+ hsh[file] = {:path => file, :mode_repo => mode_src.to_s[1, 7], :mode_index => mode_dest,
134
+ :sha_repo => sha_src, :sha_index => sha_dest, :type => type}
135
+ end
136
+ hsh
137
+ end
138
+
139
+ def ls_files
140
+ hsh = {}
141
+ lines = @base.git.ls_files({:stage => true})
142
+ lines.split("\n").each do |line|
143
+ (info, file) = line.split("\t")
144
+ (mode, sha, stage) = info.split
145
+ hsh[file] = {:path => file, :mode_index => mode, :sha_index => sha, :stage => stage}
146
+ end
147
+ hsh
148
+ end
149
+ end
150
+
151
+ end
@@ -0,0 +1,88 @@
1
+ module Grit
2
+
3
+ class Submodule
4
+ attr_reader :id
5
+ attr_reader :mode
6
+ attr_reader :name
7
+
8
+ # Create a Submodule containing just the specified attributes
9
+ # +repo+ is the Repo
10
+ # +atts+ is a Hash of instance variable data
11
+ #
12
+ # Returns Grit::Submodule (unbaked)
13
+ def self.create(repo, atts)
14
+ self.allocate.create_initialize(repo, atts)
15
+ end
16
+
17
+ # Initializer for Submodule.create
18
+ # +repo+ is the Repo
19
+ # +atts+ is a Hash of instance variable data
20
+ #
21
+ # Returns Grit::Submodule
22
+ def create_initialize(repo, atts)
23
+ @repo = repo
24
+ atts.each do |k, v|
25
+ instance_variable_set("@#{k}".to_sym, v)
26
+ end
27
+ self
28
+ end
29
+
30
+ # The url of this submodule
31
+ # +ref+ is the committish that should be used to look up the url
32
+ #
33
+ # Returns String
34
+ def url(ref)
35
+ config = self.class.config(@repo, ref)
36
+
37
+ lookup = config.keys.inject({}) do |acc, key|
38
+ id = config[key]['id']
39
+ acc[id] = config[key]['url']
40
+ acc
41
+ end
42
+
43
+ lookup[@id]
44
+ end
45
+
46
+ # The configuration information for the given +repo+
47
+ # +repo+ is the Repo
48
+ # +ref+ is the committish (defaults to 'master')
49
+ #
50
+ # Returns a Hash of { <path:String> => { 'url' => <url:String>, 'id' => <id:String> } }
51
+ # Returns {} if no .gitmodules file was found
52
+ def self.config(repo, ref = "master")
53
+ commit = repo.commit(ref)
54
+ blob = commit.tree/'.gitmodules'
55
+ return {} unless blob
56
+
57
+ lines = blob.data.gsub(/\r\n?/, "\n" ).split("\n")
58
+
59
+ config = {}
60
+ current = nil
61
+
62
+ lines.each do |line|
63
+ if line =~ /^\[submodule "(.+)"\]$/
64
+ current = $1
65
+ config[current] = {}
66
+ config[current]['id'] = (commit.tree/current).id
67
+ elsif line =~ /^\t(\w+) = (.+)$/
68
+ config[current][$1] = $2
69
+ config[current]['id'] = (commit.tree/$2).id if $1 == 'path'
70
+ else
71
+ # ignore
72
+ end
73
+ end
74
+
75
+ config
76
+ end
77
+
78
+ def basename
79
+ File.basename(name)
80
+ end
81
+
82
+ # Pretty object inspection
83
+ def inspect
84
+ %Q{#<Grit::Submodule "#{@id}">}
85
+ end
86
+ end # Submodule
87
+
88
+ end # Grit