realityforge-braid 0.7.2

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