dreamcat4-braid 0.5.1

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,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!