braid 0.5

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,173 @@
1
+ module Braid
2
+ class Mirror
3
+ TYPES = %w(git svn)
4
+ ATTRIBUTES = %w(url remote type branch squashed revision lock)
5
+
6
+ class UnknownType < BraidError
7
+ def message
8
+ "unknown type: #{super}"
9
+ end
10
+ end
11
+ class CannotGuessType < BraidError
12
+ def message
13
+ "cannot guess type: #{super}"
14
+ end
15
+ end
16
+ class PathRequired < BraidError
17
+ def message
18
+ "path is required"
19
+ end
20
+ end
21
+
22
+ include Operations::VersionControl
23
+
24
+ attr_reader :path, :attributes
25
+
26
+ def initialize(path, attributes = {})
27
+ @path = path.sub(/\/$/, '')
28
+ @attributes = attributes
29
+ end
30
+
31
+ def self.new_from_options(url, options = {})
32
+ url = url.sub(/\/$/, '')
33
+
34
+ branch = options["branch"] || "master"
35
+
36
+ if type = options["type"] || extract_type_from_url(url)
37
+ raise UnknownType, type unless TYPES.include?(type)
38
+ else
39
+ raise CannotGuessType, url
40
+ end
41
+
42
+ unless path = options["path"] || extract_path_from_url(url)
43
+ raise PathRequired
44
+ end
45
+
46
+ if options["rails_plugin"]
47
+ path = "vendor/plugins/#{path}"
48
+ end
49
+
50
+ remote = "braid/#{path}".gsub("_", '-') # stupid git svn changes all _ to ., weird
51
+ squashed = !options["full"]
52
+ branch = nil if type == "svn"
53
+
54
+ attributes = { "url" => url, "remote" => remote, "type" => type, "branch" => branch, "squashed" => squashed }
55
+ self.new(path, attributes)
56
+ end
57
+
58
+ def ==(comparison)
59
+ path == comparison.path && attributes == comparison.attributes
60
+ end
61
+
62
+ def type
63
+ # override Object#type
64
+ attributes["type"]
65
+ end
66
+
67
+ def locked?
68
+ !!lock
69
+ end
70
+
71
+ def squashed?
72
+ !!squashed
73
+ end
74
+
75
+ def merged?(commit)
76
+ # tip from spearce in #git:
77
+ # `test z$(git merge-base A B) = z$(git rev-parse --verify A)`
78
+ commit = git.rev_parse(commit)
79
+ if squashed?
80
+ !!base_revision && git.merge_base(commit, base_revision) == commit
81
+ else
82
+ git.merge_base(commit, "HEAD") == commit
83
+ end
84
+ end
85
+
86
+ def diff
87
+ remote_hash = git.rev_parse("#{base_revision}:")
88
+ local_hash = git.tree_hash(path)
89
+ remote_hash != local_hash ? git.diff_tree(remote_hash, local_hash, path) : ""
90
+ end
91
+
92
+ def fetch
93
+ unless type == "svn"
94
+ git_cache.fetch(url) if cached?
95
+ git.fetch(remote)
96
+ else
97
+ git_svn.fetch(remote)
98
+ end
99
+ end
100
+
101
+ def cached?
102
+ git.remote_url(remote) == git_cache.path(url)
103
+ end
104
+
105
+ def base_revision
106
+ if revision
107
+ unless type == "svn"
108
+ git.rev_parse(revision)
109
+ else
110
+ git_svn.commit_hash(remote, revision)
111
+ end
112
+ else
113
+ inferred_revision
114
+ end
115
+ end
116
+
117
+ private
118
+ def method_missing(name, *args)
119
+ if ATTRIBUTES.find { |attribute| name.to_s =~ /^(#{attribute})(=)?$/ }
120
+ unless $2
121
+ attributes[$1]
122
+ else
123
+ attributes[$1] = args[0]
124
+ end
125
+ else
126
+ raise NameError, "unknown attribute `#{name}'"
127
+ end
128
+ end
129
+
130
+ def inferred_revision
131
+ local_commits = git.rev_list("HEAD", "-- #{path}").split("\n")
132
+ remote_hashes = git.rev_list("--pretty=format:\"%T\"", remote).split("commit ").map do |chunk|
133
+ chunk.split("\n", 2).map { |value| value.strip }
134
+ end
135
+ hash = nil
136
+ local_commits.each do |local_commit|
137
+ local_tree = git.tree_hash(path, local_commit)
138
+ if match = remote_hashes.find { |_, remote_tree| local_tree == remote_tree }
139
+ hash = match[0]
140
+ break
141
+ end
142
+ end
143
+ hash
144
+ end
145
+
146
+ def self.extract_type_from_url(url)
147
+ return nil unless url
148
+ url.sub!(/\/$/, '')
149
+
150
+ # check for git:// and svn:// URLs
151
+ url_scheme = url.split(":").first
152
+ return url_scheme if TYPES.include?(url_scheme)
153
+
154
+ return "svn" if url[-6..-1] == "/trunk"
155
+ return "git" if url[-4..-1] == ".git"
156
+ end
157
+
158
+ def self.extract_path_from_url(url)
159
+ return nil unless url
160
+ name = File.basename(url)
161
+
162
+ if File.extname(name) == ".git"
163
+ # strip .git
164
+ name[0..-5]
165
+ elsif name == "trunk"
166
+ # use parent
167
+ File.basename(File.dirname(url))
168
+ else
169
+ name
170
+ end
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,385 @@
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
+
143
+ commit_message_file = Tempfile.new("braid_commit", ".")
144
+ commit_message_file.print("Braid: " + message)
145
+ commit_message_file.flush
146
+ status, out, err = exec("git commit -F #{commit_message_file.path} --no-verify #{args.join(' ')}")
147
+ commit_message_file.unlink
148
+
149
+ if status == 0
150
+ true
151
+ elsif out.match(/nothing.* to commit/)
152
+ false
153
+ else
154
+ raise ShellExecutionError, err
155
+ end
156
+ end
157
+
158
+ def fetch(remote = nil)
159
+ args = remote && "-n #{remote}"
160
+ # open4 messes with the pipes of index-pack
161
+ sh("git fetch #{args} 2>&1 >/dev/null")
162
+ end
163
+
164
+ def checkout(treeish)
165
+ invoke(:checkout, treeish)
166
+ true
167
+ end
168
+
169
+ # Returns the base commit or nil.
170
+ def merge_base(target, source)
171
+ invoke(:merge_base, target, source)
172
+ rescue ShellExecutionError
173
+ nil
174
+ end
175
+
176
+ def rev_parse(opt)
177
+ invoke(:rev_parse, opt)
178
+ rescue ShellExecutionError
179
+ raise UnknownRevision, opt
180
+ end
181
+
182
+ # Implies tracking.
183
+ def remote_add(remote, path, branch)
184
+ invoke(:remote, "add", "-t #{branch} -m #{branch}", remote, path)
185
+ true
186
+ end
187
+
188
+ # Checks git and svn remotes.
189
+ def remote_url(remote)
190
+ key = "remote.#{remote}.url"
191
+ begin
192
+ invoke(:config, key)
193
+ rescue ShellExecutionError
194
+ invoke(:config, "svn-#{key}")
195
+ end
196
+ rescue ShellExecutionError
197
+ nil
198
+ end
199
+
200
+ def reset_hard(target)
201
+ invoke(:reset, "--hard", target)
202
+ true
203
+ end
204
+
205
+ # Implies no commit.
206
+ def merge_ours(opt)
207
+ invoke(:merge, "-s ours --no-commit", opt)
208
+ true
209
+ end
210
+
211
+ # Implies no commit.
212
+ def merge_subtree(opt)
213
+ # TODO which options are needed?
214
+ invoke(:merge, "-s subtree --no-commit --no-ff", opt)
215
+ true
216
+ rescue ShellExecutionError
217
+ raise MergeError
218
+ end
219
+
220
+ def merge_recursive(base_hash, local_hash, remote_hash)
221
+ invoke(:merge_recursive, base_hash, "-- #{local_hash} #{remote_hash}")
222
+ true
223
+ rescue ShellExecutionError
224
+ raise MergeError
225
+ end
226
+
227
+ def read_tree_prefix(treeish, prefix)
228
+ invoke(:read_tree, "--prefix=#{prefix}/ -u", treeish)
229
+ true
230
+ end
231
+
232
+ def rm_r(path)
233
+ invoke(:rm, "-r", path)
234
+ true
235
+ end
236
+
237
+ def tree_hash(path, treeish = "HEAD")
238
+ out = invoke(:ls_tree, treeish, "-d", path)
239
+ out.split[2]
240
+ end
241
+
242
+ def diff_tree(src_tree, dst_tree, prefix = nil)
243
+ cmd = "git diff-tree -p --binary #{src_tree} #{dst_tree}"
244
+ cmd << " --src-prefix=a/#{prefix}/ --dst-prefix=b/#{prefix}/" if prefix
245
+ status, out, err = exec!(cmd)
246
+ out
247
+ end
248
+
249
+ def status_clean?
250
+ status, out, err = exec("git status")
251
+ !out.split("\n").grep(/nothing to commit/).empty?
252
+ end
253
+
254
+ def ensure_clean!
255
+ status_clean? || raise(LocalChangesPresent)
256
+ end
257
+
258
+ def head
259
+ rev_parse("HEAD")
260
+ end
261
+
262
+ def branch
263
+ status, out, err = exec!("git branch | grep '*'")
264
+ out[2..-1]
265
+ end
266
+
267
+ def apply(diff, *args)
268
+ err = nil
269
+ status = Open4.popen4("git apply --index --whitespace=nowarn #{args.join(' ')} -") do |pid, stdin, stdout, stderr|
270
+ stdin.puts(diff)
271
+ stdin.close
272
+
273
+ err = stderr.read
274
+ end.exitstatus
275
+ raise ShellExecutionError, err unless status == 0
276
+ true
277
+ end
278
+
279
+ def clone(*args)
280
+ # overrides builtin
281
+ invoke(:clone, *args)
282
+ end
283
+
284
+ private
285
+ def command(name)
286
+ "#{self.class.command} #{name.to_s.gsub('_', '-')}"
287
+ end
288
+ end
289
+
290
+ class GitSvn < Proxy
291
+ def self.command; "git svn"; end
292
+
293
+ def commit_hash(remote, revision)
294
+ out = invoke(:log, "--show-commit --oneline", "-r #{revision}", remote)
295
+ part = out.to_s.split(" | ")[1]
296
+ raise UnknownRevision, "r#{revision}" unless part
297
+ git.rev_parse(part)
298
+ end
299
+
300
+ def fetch(remote)
301
+ sh("git svn fetch #{remote} 2>&1 >/dev/null")
302
+ end
303
+
304
+ def init(remote, path)
305
+ invoke(:init, "-R", remote, "--id=#{remote}", path)
306
+ true
307
+ end
308
+
309
+ private
310
+ def command(name)
311
+ "#{self.class.command} #{name}"
312
+ end
313
+
314
+ def git
315
+ Git.instance
316
+ end
317
+ end
318
+
319
+ class Svn < Proxy
320
+ def clean_revision(revision)
321
+ revision.to_i if revision
322
+ end
323
+
324
+ def head_revision(path)
325
+ # not using svn info because it's retarded and doesn't show the actual last changed rev for the url
326
+ # git svn has no clue on how to get the actual HEAD revision number on it's own
327
+ status, out, err = exec!("svn log -q --limit 1 #{path}")
328
+ out.split(/\n/).find { |x| x.match /^r\d+/ }.split(" | ")[0][1..-1].to_i
329
+ end
330
+ end
331
+
332
+ class GitCache
333
+ include Singleton
334
+
335
+ def fetch(url)
336
+ dir = path(url)
337
+
338
+ # remove local cache if it was created with --no-checkout
339
+ if File.exists?("#{dir}/.git")
340
+ FileUtils.rm_r(dir)
341
+ end
342
+
343
+ if File.exists?(dir)
344
+ Dir.chdir(dir) do
345
+ git.fetch
346
+ end
347
+ else
348
+ FileUtils.mkdir_p(local_cache_dir)
349
+ git.clone("--mirror", url, dir)
350
+ end
351
+ end
352
+
353
+ def path(url)
354
+ File.join(local_cache_dir, url.gsub(/[\/:@]/, "_"))
355
+ end
356
+
357
+ private
358
+ def local_cache_dir
359
+ Braid.local_cache_dir
360
+ end
361
+
362
+ def git
363
+ Git.instance
364
+ end
365
+ end
366
+
367
+ module VersionControl
368
+ def git
369
+ Git.instance
370
+ end
371
+
372
+ def git_svn
373
+ GitSvn.instance
374
+ end
375
+
376
+ def svn
377
+ Svn.instance
378
+ end
379
+
380
+ def git_cache
381
+ GitCache.instance
382
+ end
383
+ end
384
+ end
385
+ end