scm 0.1.0.pre1

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