scm 0.1.0.pre1

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/scm/git.rb ADDED
@@ -0,0 +1,500 @@
1
+ require 'scm/repository'
2
+ require 'scm/commits/git'
3
+
4
+ module SCM
5
+ #
6
+ # Interacts with Git repositories.
7
+ #
8
+ class Git < Repository
9
+
10
+ # The two-letter Git status codes
11
+ STATUSES = {
12
+ ' M' => :modified,
13
+ 'M ' => :staged,
14
+ 'A ' => :added,
15
+ 'D ' => :deleted,
16
+ 'R ' => :renamed,
17
+ 'C ' => :copied,
18
+ 'U ' => :unmerged,
19
+ '??' => :untracked
20
+ }
21
+
22
+ #
23
+ # Creates a Git repository.
24
+ #
25
+ # @param [String] path
26
+ # The path to the repository.
27
+ #
28
+ # @param [Hash] options
29
+ # Additional options.
30
+ #
31
+ # @option options [Boolean] :bare
32
+ # Specifies whether to create a bare repository.
33
+ #
34
+ # @return [Git]
35
+ # The initialized Git repository.
36
+ #
37
+ # @raise [RuntimeError]
38
+ # Could not initialize the Git repository.
39
+ #
40
+ def self.create(path,options={})
41
+ path = File.expand_path(path)
42
+
43
+ arguments = []
44
+
45
+ arguments << '--bare' if options[:bare]
46
+ arguments << path
47
+
48
+ FileUtils.mkdir_p(path)
49
+
50
+ unless system('git','init',*arguments)
51
+ raise("unable to initialize Git repository #{path.dump}")
52
+ end
53
+
54
+ return new(path)
55
+ end
56
+
57
+ #
58
+ # Clones a remote Git repository.
59
+ #
60
+ # @param [URI, String] uri
61
+ # The URI of the remote repository.
62
+ #
63
+ # @param [Hash] options
64
+ # Additional options.
65
+ #
66
+ # @option options [Boolean] :bare
67
+ # Performs a bare clone of the repository.
68
+ #
69
+ # @option options [Boolean] :mirror
70
+ # Mirrors the remote repository.
71
+ #
72
+ # @option options [Integer] :depth
73
+ # Performs a shallow clone.
74
+ #
75
+ # @option options [Boolean] :submodules
76
+ # Recursively initialize each sub-module.
77
+ #
78
+ # @option options [String, Symbol] :branch
79
+ # The branch to specifically clone.
80
+ #
81
+ # @option options [String] :dest
82
+ # The destination directory to clone into.
83
+ #
84
+ # @return [Boolean]
85
+ # Specifies whether the clone was successful.
86
+ #
87
+ def self.clone(uri,options={})
88
+ arguments = []
89
+
90
+ arguments << '--bare' if options[:bare]
91
+ arguments << '--mirror' if options[:mirror]
92
+
93
+ if options[:depth]
94
+ arguments << '--depth' << options[:depth]
95
+ end
96
+
97
+ if options[:branch]
98
+ arguments << '--branch' << options[:branch]
99
+ end
100
+
101
+ arguments << '--recurse-submodules' if options[:submodules]
102
+
103
+ arguments << '--' unless arguments.empty?
104
+
105
+ arguments << uri
106
+ arguments << options[:dest] if options[:dest]
107
+
108
+ system('git','clone',*arguments)
109
+ end
110
+
111
+ #
112
+ # Queries the status of the repository.
113
+ #
114
+ # @param [Array] paths
115
+ # The optional paths to query.
116
+ #
117
+ # @return [Hash{String => Symbol}]
118
+ # The file paths and their statuses.
119
+ #
120
+ def status(*paths)
121
+ statuses = {}
122
+
123
+ popen('git status --porcelain',*paths) do |line|
124
+ status = line[0,2]
125
+ path = line[3..-1]
126
+
127
+ statuses[path] = STATUSES[status]
128
+ end
129
+
130
+ return statuses
131
+ end
132
+
133
+ #
134
+ # Adds paths to the repository.
135
+ #
136
+ # @param [Array] paths
137
+ # The paths to add to the repository.
138
+ #
139
+ def add(*paths)
140
+ git(:add,*paths)
141
+ end
142
+
143
+ #
144
+ # Moves a file or directory.
145
+ #
146
+ # @param [String] source
147
+ # The path of the source file/directory.
148
+ #
149
+ # @param [String] dest
150
+ # The new destination path.
151
+ #
152
+ # @param [Boolean] force
153
+ # Specifies whether to force the move.
154
+ #
155
+ def move(source,dest,force=false)
156
+ arguments = []
157
+
158
+ arguments << '-f' if force
159
+ arguments << source << dest
160
+
161
+ git(:mv,*arguments)
162
+ end
163
+
164
+ #
165
+ # Removes files or directories.
166
+ #
167
+ # @param [String, Array] paths
168
+ # The path(s) to remove.
169
+ #
170
+ # @param [Hash] options
171
+ # Additional options.
172
+ #
173
+ # @option options [Boolean] :force (false)
174
+ # Specifies whether to forcibly remove the files/directories.
175
+ #
176
+ # @option options [Boolean] :recursive (false)
177
+ # Specifies whether to recursively remove the files/directories.
178
+ #
179
+ def remove(paths,options={})
180
+ arguments = []
181
+
182
+ arguments << '-f' if options[:force]
183
+ arguments << '-r' if options[:recursive]
184
+ arguments += ['--', *paths]
185
+
186
+ git(:rm,*arguments)
187
+ end
188
+
189
+ #
190
+ # Makes a Git commit.
191
+ #
192
+ # @param [String] message
193
+ # The message for the commit.
194
+ #
195
+ # @param [Hash] options
196
+ # Commit options.
197
+ #
198
+ # @option options [String] :paths
199
+ # The path of the file to commit.
200
+ #
201
+ # @return [Boolean]
202
+ # Specifies whether the commit was successfully made.
203
+ #
204
+ def commit(message=nil,options={})
205
+ arguments = []
206
+
207
+ if message
208
+ arguments << '-m' << message
209
+ end
210
+
211
+ if options[:paths]
212
+ arguments += ['--', *options[:paths]]
213
+ end
214
+
215
+ git(:commit,*arguments)
216
+ end
217
+
218
+ #
219
+ # Lists Git branches.
220
+ #
221
+ # @return [Array<String>]
222
+ # The branch names.
223
+ #
224
+ def branches
225
+ branches = []
226
+
227
+ popen('git branch') do |line|
228
+ branches << line[2..-1]
229
+ end
230
+
231
+ return branches
232
+ end
233
+
234
+ #
235
+ # The current branch.
236
+ #
237
+ # @return [String]
238
+ # The name of the current branch.
239
+ #
240
+ def current_branch
241
+ popen('git branch') do |line|
242
+ return line[2..-1] if line[0,1] == '*'
243
+ end
244
+ end
245
+
246
+ #
247
+ # Swtiches to another Git branch.
248
+ #
249
+ # @param [String, Symbol] name
250
+ # The name of the branch to switch to.
251
+ #
252
+ # @return [Boolean]
253
+ # Specifies whether the branch was successfully switched.
254
+ #
255
+ def switch_branch(name)
256
+ git(:checkout,name)
257
+ end
258
+
259
+ #
260
+ # Deletes a branch.
261
+ #
262
+ # @param [String] name
263
+ # The name of the branch to delete.
264
+ #
265
+ # @return [Boolean]
266
+ # Specifies whether the branch was successfully deleted.
267
+ #
268
+ def delete_branch(name)
269
+ git(:branch,'-d',name)
270
+ end
271
+
272
+ #
273
+ # Lists Git tags.
274
+ #
275
+ # @return [Array<String>]
276
+ # The tag names.
277
+ #
278
+ def tags
279
+ tags = []
280
+
281
+ popen('git tag') do |line|
282
+ tags << line[2..-1]
283
+ end
284
+
285
+ return tags
286
+ end
287
+
288
+ #
289
+ # Creates a Git tag.
290
+ #
291
+ # @param [String] name
292
+ # The name for the tag.
293
+ #
294
+ # @param [String] commit
295
+ # The commit to create the tag at.
296
+ #
297
+ # @return [Boolean]
298
+ # Specifies whether the tag was successfully created.
299
+ #
300
+ def tag(name,commit=nil)
301
+ arguments = []
302
+ arguments << commit if commit
303
+
304
+ git(:tag,name,*arguments)
305
+ end
306
+
307
+ #
308
+ # Deletes a Git tag.
309
+ #
310
+ # @param [String] name
311
+ # The name of the tag.
312
+ #
313
+ # @return [Boolean]
314
+ # Specifies whether the tag was successfully deleted.
315
+ #
316
+ def delete_tag(name)
317
+ git(:tag,'-d',name)
318
+ end
319
+
320
+ #
321
+ # Prints the Git log.
322
+ #
323
+ # @param [String] :commit
324
+ # Commit to begin the log at.
325
+ #
326
+ # @param [String] :paths
327
+ # File to list commits for.
328
+ #
329
+ def log(options={})
330
+ arguments = []
331
+
332
+ arguments << options[:commit] if options[:commit]
333
+
334
+ if options[:paths]
335
+ arguments += ['--', *options[:paths]]
336
+ end
337
+
338
+ git(:log,*arguments)
339
+ end
340
+
341
+ #
342
+ # Pushes changes to the remote Git repository.
343
+ #
344
+ # @param [Hash] options
345
+ # Additional options.
346
+ #
347
+ # @option options [Boolean] :mirror
348
+ # Specifies to push all refs under `.git/refs/`.
349
+ #
350
+ # @option options [Boolean] :all
351
+ # Specifies to push all refs under `.git/refs/heads/`.
352
+ #
353
+ # @option options [Boolean] :tags
354
+ # Specifies to push all tags.
355
+ #
356
+ # @option options [Boolean] :force
357
+ # Specifies whether to force pushing the changes.
358
+ #
359
+ # @option options [String, Symbol] :repository
360
+ # The remote repository to push to.
361
+ #
362
+ # @option options [String, Symbol] :branch
363
+ # The specific branch to push.
364
+ #
365
+ # @return [Boolean]
366
+ # Specifies whether the changes were successfully pushed.
367
+ #
368
+ def push(options={})
369
+ arguments = []
370
+
371
+ if options[:mirror]
372
+ arguments << '--mirror'
373
+ elsif options[:all]
374
+ arguments << '--all'
375
+ elsif options[:tags]
376
+ arguments << '--tags'
377
+ end
378
+
379
+ arguments << '-f' if options[:force]
380
+ arguments << options[:repository] if options[:repository]
381
+
382
+ if options[:branch]
383
+ arguments << 'origin' unless options[:repository]
384
+ arguments << options[:branch]
385
+ end
386
+
387
+ git(:push,*arguments)
388
+ end
389
+
390
+ #
391
+ # Pulls changes from the remote Git repository.
392
+ #
393
+ # @param [Hash] options
394
+ # Additional options.
395
+ #
396
+ # @option options [Boolean] :force
397
+ # Specifies whether to force pushing the changes.
398
+ #
399
+ # @option options [String, Symbol] :repository
400
+ # The remote repository to push to.
401
+ #
402
+ # @return [Boolean]
403
+ # Specifies whether the changes were successfully pulled.
404
+ #
405
+ def pull(options={})
406
+ arguments = []
407
+
408
+ arguments << '-f' if options[:force]
409
+ arguments << options[:repository] if options[:repository]
410
+
411
+ git(:pull,*arguments)
412
+ end
413
+
414
+ #
415
+ # Lists the commits in the Git repository.
416
+ #
417
+ # @param [Hash] options
418
+ # Additional options.
419
+ #
420
+ # @option options [String] :commit
421
+ # Commit to start at.
422
+ #
423
+ # @option options [Symbol, String] :branch
424
+ # The branch to list commits within.
425
+ #
426
+ # @option options [Integer] :limit
427
+ # The number of commits to list.
428
+ #
429
+ # @option options [String, Array<String>] :paths
430
+ # The path(s) to list commits for.
431
+ #
432
+ # @yield [commit]
433
+ # The given block will be passed each commit.
434
+ #
435
+ # @yieldparam [Commits::Git] commit
436
+ # A commit from the repository.
437
+ #
438
+ # @return [Enumerator<Commits::Git>]
439
+ # The commits in the repository.
440
+ #
441
+ def commits(options={})
442
+ return enum_for(:commits,options) unless block_given?
443
+
444
+ arguments = ["--pretty=format:%H|%P|%T|%at|%an|%ae|%s"]
445
+
446
+ if options[:limit]
447
+ arguments << "-#{options[:limit]}"
448
+ end
449
+
450
+ if (options[:commit] || options[:branch])
451
+ arguments << (options[:commit] || options[:branch])
452
+ end
453
+
454
+ if options[:paths]
455
+ arguments += ['--', *options[:paths]]
456
+ end
457
+
458
+ commit = nil
459
+ parent = nil
460
+ tree = nil
461
+ tree = nil
462
+ parent = nil
463
+ author = nil
464
+ date = nil
465
+
466
+ popen('git log',*arguments) do |line|
467
+ commit, parent, tree, date, author, email, summary = line.split('|',7)
468
+
469
+ yield Commits::Git.new(
470
+ commit,
471
+ parent,
472
+ tree,
473
+ Time.at(date.to_i),
474
+ author,
475
+ email,
476
+ summary
477
+ )
478
+ end
479
+ end
480
+
481
+ protected
482
+
483
+ #
484
+ # Runs a Git command.
485
+ #
486
+ # @param [Symbol] command
487
+ # The Git command to run.
488
+ #
489
+ # @param [Array] arguments
490
+ # Additional arguments to pass to the Git command.
491
+ #
492
+ # @return [Boolean]
493
+ # Specifies whether the Git command exited successfully.
494
+ #
495
+ def git(command,*arguments)
496
+ run(:git,command,*arguments)
497
+ end
498
+
499
+ end
500
+ end