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