braid 0.5

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