dreamcat4-braid 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,456 @@
1
+ require 'singleton'
2
+ require 'rubygems'
3
+ require 'open4'
4
+ require 'tempfile'
5
+
6
+ module Braid
7
+ module Operations
8
+ class ShellExecutionError < BraidError
9
+ def initialize(err = nil)
10
+ @err = err
11
+ end
12
+
13
+ def message
14
+ @err.to_s.split("\n").first
15
+ end
16
+ end
17
+ class VersionTooLow < BraidError
18
+ def initialize(command, version, required)
19
+ @command = command
20
+ @version = version.to_s.split("\n").first
21
+ @required = required
22
+ end
23
+
24
+ def message
25
+ "#{@command} version too low: #{@version}. #{@required} needed."
26
+ end
27
+ end
28
+ class UnknownRevision < BraidError
29
+ def message
30
+ "unknown revision: #{super}"
31
+ end
32
+ end
33
+ class LocalChangesPresent < BraidError
34
+ def message
35
+ "local changes are present"
36
+ end
37
+ end
38
+ class MergeError < BraidError
39
+ def message
40
+ "could not merge"
41
+ end
42
+ end
43
+
44
+ # The command proxy is meant to encapsulate commands such as git, git-svn and svn, that work with subcommands.
45
+ class Proxy
46
+ include Singleton
47
+
48
+ def self.command; name.split('::').last.downcase; end # hax!
49
+
50
+ def version
51
+ status, out, err = exec!("#{self.class.command} --version")
52
+ out.sub(/^.* version/, "").strip
53
+ end
54
+
55
+ def require_version(required)
56
+ required = required.split(".")
57
+ actual = version.split(".")
58
+
59
+ actual.each_with_index do |actual_piece, idx|
60
+ required_piece = required[idx]
61
+
62
+ return true unless required_piece
63
+
64
+ case (actual_piece <=> required_piece)
65
+ when -1
66
+ return false
67
+ when 1
68
+ return true
69
+ when 0
70
+ next
71
+ end
72
+ end
73
+
74
+ return actual.length >= required.length
75
+ end
76
+
77
+ def require_version!(required)
78
+ require_version(required) || raise(VersionTooLow.new(self.class.command, version, required))
79
+ end
80
+
81
+ private
82
+ def command(name)
83
+ # stub
84
+ name
85
+ end
86
+
87
+ def invoke(arg, *args)
88
+ exec!("#{command(arg)} #{args.join(' ')}".strip)[1].strip # return stdout
89
+ end
90
+
91
+ def method_missing(name, *args)
92
+ invoke(name, *args)
93
+ end
94
+
95
+ def exec(cmd)
96
+ cmd.strip!
97
+
98
+ previous_lang = ENV['LANG']
99
+ ENV['LANG'] = 'C'
100
+
101
+ out, err = nil
102
+ log(cmd)
103
+ status = Open4.popen4(cmd) do |pid, stdin, stdout, stderr|
104
+ out = stdout.read
105
+ err = stderr.read
106
+ end.exitstatus
107
+ [status, out, err]
108
+
109
+ ensure
110
+ ENV['LANG'] = previous_lang
111
+ end
112
+
113
+ def exec!(cmd)
114
+ status, out, err = exec(cmd)
115
+ raise ShellExecutionError, err unless status == 0
116
+ [status, out, err]
117
+ end
118
+
119
+ def sh(cmd, message = nil)
120
+ message ||= "could not fetch" if cmd =~ /fetch/
121
+ log(cmd)
122
+ `#{cmd}`
123
+ raise ShellExecutionError, message unless $?.exitstatus == 0
124
+ true
125
+ end
126
+
127
+ def msg(str)
128
+ puts "Braid: #{str}"
129
+ end
130
+
131
+ def log(cmd)
132
+ msg "Executing `#{cmd}`" if verbose?
133
+ end
134
+
135
+ def verbose?
136
+ Braid.verbose
137
+ end
138
+ end
139
+
140
+ class GitClone < Proxy
141
+ def in_rep_root_check
142
+ if ! File.exists?(".git")
143
+ raise("Not in root repository.")
144
+ end
145
+ end
146
+
147
+ def add_gitignore(path)
148
+ # add mirror to .gitignore file
149
+ in_rep_root_check
150
+ if ! File.exists?(".gitignore")
151
+ f = File.new(".gitignore", "w+")
152
+ else
153
+ f = File.open( 'index', 'w+')
154
+ end
155
+
156
+ f.each { |line|
157
+ if line == path
158
+ path_ignored = line
159
+ end
160
+ }
161
+ if ! ignored
162
+ f.puts path
163
+ git.add(".gitignore")
164
+ end
165
+ f.close
166
+ end
167
+
168
+ def remove_gitignore(path)
169
+ # remove mirror from .gitignore file
170
+ in_rep_root_check
171
+ if File.exists?(".gitignore")
172
+ f = File.open( 'index', 'w+')
173
+
174
+ f.each { |line|
175
+ if line == path
176
+ path_ignored = line
177
+ end
178
+ }
179
+ f.rewind
180
+
181
+ if path_ignored
182
+ date_str= Date.new.to_s
183
+ n = File.new(".gitignore-#{date_str}", "w+")
184
+ f.each { |line|
185
+ n.puts line unless line == path_ignored
186
+ }
187
+ n.close
188
+ end
189
+ f.close
190
+ File.rename( ".gitignore-#{date_str}", ".gitignore" )
191
+ git.add(".gitignore")
192
+ end
193
+
194
+ end
195
+
196
+ private
197
+ def command(name)
198
+ "#{self.class.command} #{name}"
199
+ end
200
+
201
+ def git
202
+ GitClone.instance
203
+ end
204
+ end
205
+
206
+ class Git < Proxy
207
+ def commit(message, *args)
208
+
209
+ commit_message_file = Tempfile.new("braid_commit", ".")
210
+ commit_message_file.print("Braid: " + message)
211
+ commit_message_file.flush
212
+ status, out, err = exec("git commit -F #{commit_message_file.path} --no-verify #{args.join(' ')}")
213
+ commit_message_file.unlink
214
+
215
+ if status == 0
216
+ true
217
+ elsif out.match(/nothing.* to commit/)
218
+ false
219
+ else
220
+ raise ShellExecutionError, err
221
+ end
222
+ end
223
+
224
+ def fetch(remote = nil)
225
+ args = remote && "-n #{remote}"
226
+ # open4 messes with the pipes of index-pack
227
+ sh("git fetch #{args} 2>&1 >/dev/null")
228
+ end
229
+
230
+ def checkout(treeish)
231
+ invoke(:checkout, treeish)
232
+ true
233
+ end
234
+
235
+ # Returns the base commit or nil.
236
+ def merge_base(target, source)
237
+ invoke(:merge_base, target, source)
238
+ rescue ShellExecutionError
239
+ nil
240
+ end
241
+
242
+ def rev_parse(opt)
243
+ invoke(:rev_parse, opt)
244
+ rescue ShellExecutionError
245
+ raise UnknownRevision, opt
246
+ end
247
+
248
+ # Implies tracking.
249
+ def remote_add(remote, path, branch)
250
+ invoke(:remote, "add", "-t #{branch} -m #{branch}", remote, path)
251
+ true
252
+ end
253
+
254
+ def remote_rm(remote)
255
+ invoke(:remote, "rm", remote)
256
+ true
257
+ end
258
+
259
+ # Checks git and svn remotes.
260
+ def remote_url(remote)
261
+ key = "remote.#{remote}.url"
262
+ begin
263
+ invoke(:config, key)
264
+ rescue ShellExecutionError
265
+ invoke(:config, "svn-#{key}")
266
+ end
267
+ rescue ShellExecutionError
268
+ nil
269
+ end
270
+
271
+ def reset_hard(target)
272
+ invoke(:reset, "--hard", target)
273
+ true
274
+ end
275
+
276
+ # Implies no commit.
277
+ def merge_ours(opt)
278
+ invoke(:merge, "-s ours --no-commit", opt)
279
+ true
280
+ end
281
+
282
+ # Implies no commit.
283
+ def merge_subtree(opt)
284
+ # TODO which options are needed?
285
+ invoke(:merge, "-s subtree --no-commit --no-ff", opt)
286
+ true
287
+ rescue ShellExecutionError
288
+ raise MergeError
289
+ end
290
+
291
+ def merge_recursive(base_hash, local_hash, remote_hash)
292
+ invoke(:merge_recursive, base_hash, "-- #{local_hash} #{remote_hash}")
293
+ true
294
+ rescue ShellExecutionError
295
+ raise MergeError
296
+ end
297
+
298
+ def read_tree_prefix(treeish, prefix)
299
+ invoke(:read_tree, "--prefix=#{prefix}/ -u", treeish)
300
+ true
301
+ end
302
+
303
+ def rm_r(path)
304
+ invoke(:rm, "-r", path)
305
+ true
306
+ end
307
+
308
+ def tree_hash(path, treeish = "HEAD")
309
+ out = invoke(:ls_tree, treeish, "-d", path)
310
+ out.split[2]
311
+ end
312
+
313
+ def diff_tree(src_tree, dst_tree, prefix = nil)
314
+ cmd = "git diff-tree -p --binary #{src_tree} #{dst_tree}"
315
+ cmd << " --src-prefix=a/#{prefix}/ --dst-prefix=b/#{prefix}/" if prefix
316
+ status, out, err = exec!(cmd)
317
+ out
318
+ end
319
+
320
+ def status_clean?
321
+ status, out, err = exec("git status")
322
+ !out.split("\n").grep(/nothing to commit/).empty?
323
+ end
324
+
325
+ def ensure_clean!
326
+ status_clean? || raise(LocalChangesPresent)
327
+ end
328
+
329
+ def head
330
+ rev_parse("HEAD")
331
+ end
332
+
333
+ def branch
334
+ status, out, err = exec!("git branch | grep '*'")
335
+ out[2..-1]
336
+ end
337
+
338
+ def apply(diff, *args)
339
+ err = nil
340
+ status = Open4.popen4("git apply --index --whitespace=nowarn #{args.join(' ')} -") do |pid, stdin, stdout, stderr|
341
+ stdin.puts(diff)
342
+ stdin.close
343
+
344
+ err = stderr.read
345
+ end.exitstatus
346
+ raise ShellExecutionError, err unless status == 0
347
+ true
348
+ end
349
+
350
+ def clone(*args)
351
+ # overrides builtin
352
+ invoke(:clone, *args)
353
+ end
354
+
355
+ private
356
+ def command(name)
357
+ "#{self.class.command} #{name.to_s.gsub('_', '-')}"
358
+ end
359
+ end
360
+
361
+ class GitSvn < Proxy
362
+ def self.command; "git svn"; end
363
+
364
+ def commit_hash(remote, revision)
365
+ out = invoke(:log, "--show-commit --oneline", "-r #{revision}", remote)
366
+ part = out.to_s.split(" | ")[1]
367
+ raise UnknownRevision, "r#{revision}" unless part
368
+ git.rev_parse(part)
369
+ end
370
+
371
+ def fetch(remote)
372
+ sh("git svn fetch #{remote} 2>&1 >/dev/null")
373
+ end
374
+
375
+ def init(remote, path)
376
+ invoke(:init, "-R", remote, "--id=#{remote}", path)
377
+ true
378
+ end
379
+
380
+ private
381
+ def command(name)
382
+ "#{self.class.command} #{name}"
383
+ end
384
+
385
+ def git
386
+ Git.instance
387
+ end
388
+ end
389
+
390
+ class Svn < Proxy
391
+ def clean_revision(revision)
392
+ revision.to_i if revision
393
+ end
394
+
395
+ def head_revision(path)
396
+ # not using svn info because it's retarded and doesn't show the actual last changed rev for the url
397
+ # git svn has no clue on how to get the actual HEAD revision number on it's own
398
+ status, out, err = exec!("svn log -q --limit 1 #{path}")
399
+ out.split(/\n/).find { |x| x.match /^r\d+/ }.split(" | ")[0][1..-1].to_i
400
+ end
401
+ end
402
+
403
+ class GitCache
404
+ include Singleton
405
+
406
+ def fetch(url)
407
+ dir = path(url)
408
+
409
+ # remove local cache if it was created with --no-checkout
410
+ if File.exists?("#{dir}/.git")
411
+ FileUtils.rm_r(dir)
412
+ end
413
+
414
+ if File.exists?(dir)
415
+ Dir.chdir(dir) do
416
+ git.fetch
417
+ end
418
+ else
419
+ FileUtils.mkdir_p(local_cache_dir)
420
+ git.clone("--mirror", url, dir)
421
+ end
422
+ end
423
+
424
+ def path(url)
425
+ File.join(local_cache_dir, url.gsub(/[\/:@]/, "_"))
426
+ end
427
+
428
+ private
429
+ def local_cache_dir
430
+ Braid.local_cache_dir
431
+ end
432
+
433
+ def git
434
+ Git.instance
435
+ end
436
+ end
437
+
438
+ module VersionControl
439
+ def git
440
+ Git.instance
441
+ end
442
+
443
+ def git_svn
444
+ GitSvn.instance
445
+ end
446
+
447
+ def svn
448
+ Svn.instance
449
+ end
450
+
451
+ def git_cache
452
+ GitCache.instance
453
+ end
454
+ end
455
+ end
456
+ end
data/lib/braid.rb ADDED
@@ -0,0 +1,29 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
3
+ module Braid
4
+ VERSION = "0.5"
5
+
6
+ CONFIG_FILE = ".braids"
7
+ REQUIRED_GIT_VERSION = "1.6"
8
+
9
+ def self.verbose; @verbose || false; end
10
+ def self.verbose=(new_value); @verbose = !!new_value; end
11
+
12
+ def self.use_local_cache; [nil, "true", "1"].include?(ENV["BRAID_USE_LOCAL_CACHE"]); end
13
+ def self.local_cache_dir; File.expand_path(ENV["BRAID_LOCAL_CACHE_DIR"] || "#{ENV["HOME"]}/.braid/cache"); end
14
+
15
+ class BraidError < StandardError
16
+ def message
17
+ value = super
18
+ value if value != self.class.name
19
+ end
20
+ end
21
+ end
22
+
23
+ require 'braid/operations'
24
+ require 'braid/mirror'
25
+ require 'braid/config'
26
+ require 'braid/command'
27
+ Dir[File.dirname(__FILE__) + '/braid/commands/*'].each do |file|
28
+ require file
29
+ end
@@ -0,0 +1,7 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ describe "Braid" do
4
+ it "puts the lotion in the basket" do
5
+ # dedicated to dblack
6
+ end
7
+ end
@@ -0,0 +1,62 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ describe_shared "Braid::Config, in general" do
4
+ db = "tmp.yml"
5
+
6
+ before(:each) do
7
+ @config = Braid::Config.new(db)
8
+ end
9
+
10
+ after(:each) do
11
+ FileUtils.rm(db) rescue nil
12
+ end
13
+ end
14
+
15
+ describe "Braid::Config, when empty" do
16
+ it_should_behave_like "Braid::Config, in general"
17
+
18
+ it "should not get a mirror by name" do
19
+ @config.get("path").should.be.nil
20
+ lambda { @config.get!("path") }.should.raise(Braid::Config::MirrorDoesNotExist)
21
+ end
22
+
23
+ it "should add a mirror and its params" do
24
+ @mirror = build_mirror
25
+ @config.add(@mirror)
26
+ @config.get("path").path.should.not.be.nil
27
+ end
28
+ end
29
+
30
+ describe "Braid::Config, with one mirror" do
31
+ it_should_behave_like "Braid::Config, in general"
32
+
33
+ before(:each) do
34
+ @mirror = build_mirror
35
+ @config.add(@mirror)
36
+ end
37
+
38
+ it "should get the mirror by name" do
39
+ @config.get("path").should == @mirror
40
+ @config.get!("path").should == @mirror
41
+ end
42
+
43
+ it "should raise when trying to overwrite a mirror on add" do
44
+ lambda { @config.add(@mirror) }.should.raise(Braid::Config::PathAlreadyInUse)
45
+ end
46
+
47
+ it "should remove the mirror" do
48
+ @config.remove(@mirror)
49
+ @config.get("path").should.be.nil
50
+ end
51
+
52
+ it "should update the mirror with new params" do
53
+ @mirror.branch = "other"
54
+ @config.update(@mirror)
55
+ @config.get("path").attributes.should == { "branch" => "other" }
56
+ end
57
+
58
+ it "should raise when trying to update nonexistent mirror" do
59
+ @mirror.instance_variable_set("@path", "other")
60
+ lambda { @config.update(@mirror) }.should.raise(Braid::Config::MirrorDoesNotExist)
61
+ end
62
+ end
@@ -0,0 +1,3 @@
1
+ shiny app
2
+
3
+ it uses braid to pull in a bunch of stuff!