honkster-braid 0.6.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,394 @@
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 Git < Proxy
141
+ def commit(message, *args)
142
+ cmd = "git commit --no-verify"
143
+ if message # allow nil
144
+ message_file = Tempfile.new("braid_commit")
145
+ message_file.print("Braid: #{message}")
146
+ message_file.flush
147
+ cmd << " -F #{message_file.path}"
148
+ end
149
+ cmd << " #{args.join(' ')}" unless args.empty?
150
+ status, out, err = exec(cmd)
151
+ message_file.unlink if message_file
152
+
153
+ if status == 0
154
+ true
155
+ elsif out.match(/nothing.* to commit/)
156
+ false
157
+ else
158
+ raise ShellExecutionError, err
159
+ end
160
+ end
161
+
162
+ def fetch(remote = nil, *args)
163
+ args.unshift "-n #{remote}" if remote
164
+ # open4 messes with the pipes of index-pack
165
+ sh("git fetch #{args.join(' ')} 2>&1 >/dev/null")
166
+ end
167
+
168
+ def checkout(treeish)
169
+ invoke(:checkout, treeish)
170
+ true
171
+ end
172
+
173
+ # Returns the base commit or nil.
174
+ def merge_base(target, source)
175
+ invoke(:merge_base, target, source)
176
+ rescue ShellExecutionError
177
+ nil
178
+ end
179
+
180
+ def rev_parse(opt)
181
+ invoke(:rev_parse, opt)
182
+ rescue ShellExecutionError
183
+ raise UnknownRevision, opt
184
+ end
185
+
186
+ # Implies tracking.
187
+ def remote_add(remote, path, branch)
188
+ invoke(:remote, "add", "-t #{branch} -m #{branch}", remote, path)
189
+ true
190
+ end
191
+
192
+ def remote_rm(remote)
193
+ invoke(:remote, "rm", remote)
194
+ true
195
+ end
196
+
197
+ # Checks git and svn remotes.
198
+ def remote_url(remote)
199
+ key = "remote.#{remote}.url"
200
+ begin
201
+ invoke(:config, key)
202
+ rescue ShellExecutionError
203
+ invoke(:config, "svn-#{key}")
204
+ end
205
+ rescue ShellExecutionError
206
+ nil
207
+ end
208
+
209
+ def reset_hard(target)
210
+ invoke(:reset, "--hard", target)
211
+ true
212
+ end
213
+
214
+ # Implies no commit.
215
+ def merge_ours(opt)
216
+ invoke(:merge, "-s ours --no-commit", opt)
217
+ true
218
+ end
219
+
220
+ # Implies no commit.
221
+ def merge_subtree(opt)
222
+ # TODO which options are needed?
223
+ invoke(:merge, "-s subtree --no-commit --no-ff", opt)
224
+ true
225
+ rescue ShellExecutionError
226
+ raise MergeError
227
+ end
228
+
229
+ def merge_recursive(base_hash, local_hash, remote_hash)
230
+ invoke(:merge_recursive, base_hash, "-- #{local_hash} #{remote_hash}")
231
+ true
232
+ rescue ShellExecutionError
233
+ raise MergeError
234
+ end
235
+
236
+ def read_tree_prefix(treeish, prefix)
237
+ invoke(:read_tree, "--prefix=#{prefix}/ -u", treeish)
238
+ true
239
+ end
240
+
241
+ def rm_r(path)
242
+ invoke(:rm, "-r", path)
243
+ true
244
+ end
245
+
246
+ def tree_hash(path, treeish = "HEAD")
247
+ out = invoke(:ls_tree, treeish, "-d", path)
248
+ out.split[2]
249
+ end
250
+
251
+ def diff_tree(src_tree, dst_tree, prefix = nil)
252
+ cmd = "git diff-tree -p --binary #{src_tree} #{dst_tree}"
253
+ cmd << " --src-prefix=a/#{prefix}/ --dst-prefix=b/#{prefix}/" if prefix
254
+ status, out, err = exec!(cmd)
255
+ out
256
+ end
257
+
258
+ def status_clean?
259
+ status, out, err = exec("git status")
260
+ !out.split("\n").grep(/nothing to commit/).empty?
261
+ end
262
+
263
+ def ensure_clean!
264
+ status_clean? || raise(LocalChangesPresent)
265
+ end
266
+
267
+ def head
268
+ rev_parse("HEAD")
269
+ end
270
+
271
+ def branch
272
+ status, out, err = exec!("git branch | grep '*'")
273
+ out[2..-1]
274
+ end
275
+
276
+ def apply(diff, *args)
277
+ err = nil
278
+ status = Open4.popen4("git apply --index --whitespace=nowarn #{args.join(' ')} -") do |pid, stdin, stdout, stderr|
279
+ stdin.puts(diff)
280
+ stdin.close
281
+
282
+ err = stderr.read
283
+ end.exitstatus
284
+ raise ShellExecutionError, err unless status == 0
285
+ true
286
+ end
287
+
288
+ def clone(*args)
289
+ # overrides builtin
290
+ invoke(:clone, *args)
291
+ end
292
+
293
+ private
294
+ def command(name)
295
+ "#{self.class.command} #{name.to_s.gsub('_', '-')}"
296
+ end
297
+ end
298
+
299
+ class GitSvn < Proxy
300
+ def self.command; "git svn"; end
301
+
302
+ def commit_hash(remote, revision)
303
+ out = invoke(:log, "--show-commit --oneline", "-r #{revision}", remote)
304
+ part = out.to_s.split(" | ")[1]
305
+ raise UnknownRevision, "r#{revision}" unless part
306
+ git.rev_parse(part)
307
+ end
308
+
309
+ def fetch(remote)
310
+ sh("git svn fetch #{remote} 2>&1 >/dev/null")
311
+ end
312
+
313
+ def init(remote, path)
314
+ invoke(:init, "-R", remote, "--id=#{remote}", path)
315
+ true
316
+ end
317
+
318
+ private
319
+ def command(name)
320
+ "#{self.class.command} #{name}"
321
+ end
322
+
323
+ def git
324
+ Git.instance
325
+ end
326
+ end
327
+
328
+ class Svn < Proxy
329
+ def clean_revision(revision)
330
+ revision.to_i if revision
331
+ end
332
+
333
+ def head_revision(path)
334
+ # not using svn info because it's retarded and doesn't show the actual last changed rev for the url
335
+ # git svn has no clue on how to get the actual HEAD revision number on it's own
336
+ status, out, err = exec!("svn log -q --limit 1 #{path}")
337
+ out.split(/\n/).find { |x| x.match /^r\d+/ }.split(" | ")[0][1..-1].to_i
338
+ end
339
+ end
340
+
341
+ class GitCache
342
+ include Singleton
343
+
344
+ def fetch(url)
345
+ dir = path(url)
346
+
347
+ # remove local cache if it was created with --no-checkout
348
+ if File.exists?("#{dir}/.git")
349
+ FileUtils.rm_r(dir)
350
+ end
351
+
352
+ if File.exists?(dir)
353
+ Dir.chdir(dir) do
354
+ git.fetch
355
+ end
356
+ else
357
+ FileUtils.mkdir_p(local_cache_dir)
358
+ git.clone("--mirror", url, dir)
359
+ end
360
+ end
361
+
362
+ def path(url)
363
+ File.join(local_cache_dir, url.gsub(/[\/:@]/, "_"))
364
+ end
365
+
366
+ private
367
+ def local_cache_dir
368
+ Braid.local_cache_dir
369
+ end
370
+
371
+ def git
372
+ Git.instance
373
+ end
374
+ end
375
+
376
+ module VersionControl
377
+ def git
378
+ Git.instance
379
+ end
380
+
381
+ def git_svn
382
+ GitSvn.instance
383
+ end
384
+
385
+ def svn
386
+ Svn.instance
387
+ end
388
+
389
+ def git_cache
390
+ GitCache.instance
391
+ end
392
+ end
393
+ end
394
+ 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