realityforge-braid 0.7.2

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