norbert-braid 0.4.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,101 @@
1
+ require 'yaml'
2
+ require 'yaml/store'
3
+
4
+ module Braid
5
+ class Config
6
+ class PathAlreadyInUse < BraidError
7
+ def message
8
+ "path already in use: #{super}"
9
+ end
10
+ end
11
+ class MirrorDoesNotExist < BraidError
12
+ def message
13
+ "mirror does not exist: #{super}"
14
+ end
15
+ end
16
+
17
+ def initialize(config_file = CONFIG_FILE)
18
+ @db = YAML::Store.new(config_file)
19
+ end
20
+
21
+ def add_from_options(url, options)
22
+ mirror = Mirror.new_from_options(url, options)
23
+
24
+ add(mirror)
25
+ mirror
26
+ end
27
+
28
+ def mirrors
29
+ @db.transaction(true) do
30
+ @db.roots
31
+ end
32
+ end
33
+
34
+ def get(path)
35
+ @db.transaction(true) do
36
+ if attributes = @db[path.to_s.sub(/\/$/, '')]
37
+ Mirror.new(path, attributes)
38
+ end
39
+ end
40
+ end
41
+
42
+ def get!(path)
43
+ mirror = get(path)
44
+ raise MirrorDoesNotExist, path unless mirror
45
+ mirror
46
+ end
47
+
48
+ def add(mirror)
49
+ @db.transaction do
50
+ raise PathAlreadyInUse, mirror.path if @db[mirror.path]
51
+ write_mirror(mirror)
52
+ end
53
+ end
54
+
55
+ def remove(mirror)
56
+ @db.transaction do
57
+ @db.delete(mirror.path)
58
+ end
59
+ end
60
+
61
+ def update(mirror)
62
+ @db.transaction do
63
+ raise MirrorDoesNotExist, mirror.path unless @db[mirror.path]
64
+ @db.delete(mirror.path)
65
+ write_mirror(mirror)
66
+ end
67
+ end
68
+
69
+ def valid?
70
+ @db.transaction(true) do
71
+ !@db.roots.any? do |path|
72
+ @db[path]["url"].nil?
73
+ end
74
+ end
75
+ end
76
+
77
+ def migrate!
78
+ @db.transaction do
79
+ @db.roots.each do |path|
80
+ attributes = @db[path]
81
+ if attributes["local_branch"]
82
+ attributes["url"] = attributes.delete("remote")
83
+ attributes["remote"] = attributes.delete("local_branch")
84
+ attributes["squashed"] = attributes.delete("squash")
85
+ attributes["lock"] = attributes["revision"] # so far this has always been true
86
+ end
87
+ @db[path] = clean_attributes(attributes)
88
+ end
89
+ end
90
+ end
91
+
92
+ private
93
+ def write_mirror(mirror)
94
+ @db[mirror.path] = clean_attributes(mirror.attributes)
95
+ end
96
+
97
+ def clean_attributes(hash)
98
+ hash.reject { |k,v| v.nil? }
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,168 @@
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.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.fetch(remote)
95
+ else
96
+ git_svn.fetch(remote)
97
+ end
98
+ end
99
+
100
+ private
101
+ def method_missing(name, *args)
102
+ if ATTRIBUTES.find { |attribute| name.to_s =~ /^(#{attribute})(=)?$/ }
103
+ unless $2
104
+ attributes[$1]
105
+ else
106
+ attributes[$1] = args[0]
107
+ end
108
+ else
109
+ raise NameError, "unknown attribute `#{name}'"
110
+ end
111
+ end
112
+
113
+ def base_revision
114
+ if revision
115
+ unless type == "svn"
116
+ git.rev_parse(revision)
117
+ else
118
+ git_svn.commit_hash(remote, revision)
119
+ end
120
+ else
121
+ inferred_revision
122
+ end
123
+ end
124
+
125
+ def inferred_revision
126
+ local_commits = git.rev_list("HEAD", "-- #{path}").split("\n")
127
+ remote_hashes = git.rev_list("--pretty=format:\"%T\"", remote).split("commit ").map do |chunk|
128
+ chunk.split("\n", 2).map { |value| value.strip }
129
+ end
130
+ hash = nil
131
+ local_commits.each do |local_commit|
132
+ local_tree = git.tree_hash(path, local_commit)
133
+ if match = remote_hashes.find { |_, remote_tree| local_tree == remote_tree }
134
+ hash = match[0]
135
+ break
136
+ end
137
+ end
138
+ hash
139
+ end
140
+
141
+ def self.extract_type_from_url(url)
142
+ return nil unless url
143
+ url.sub!(/\/$/, '')
144
+
145
+ # check for git:// and svn:// URLs
146
+ url_scheme = url.split(":").first
147
+ return url_scheme if TYPES.include?(url_scheme)
148
+
149
+ return "svn" if url[-6..-1] == "/trunk"
150
+ return "git" if url[-4..-1] == ".git"
151
+ end
152
+
153
+ def self.extract_path_from_url(url)
154
+ return nil unless url
155
+ name = File.basename(url)
156
+
157
+ if File.extname(name) == ".git"
158
+ # strip .git
159
+ name[0..-5]
160
+ elsif name == "trunk"
161
+ # use parent
162
+ File.basename(File.dirname(url))
163
+ else
164
+ name
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,297 @@
1
+ require 'singleton'
2
+ require 'rubygems'
3
+ require 'open4'
4
+
5
+ module Braid
6
+ module Operations
7
+ class ShellExecutionError < BraidError
8
+ def initialize(err = nil)
9
+ @err = err
10
+ end
11
+
12
+ def message
13
+ @err.to_s.split("\n").first
14
+ end
15
+ end
16
+ class VersionTooLow < BraidError
17
+ def initialize(command, version)
18
+ @command = command
19
+ @version = version.to_s.split("\n").first
20
+ end
21
+
22
+ def message
23
+ "#{@command} version too low: #{@version}"
24
+ end
25
+ end
26
+ class UnknownRevision < BraidError
27
+ def message
28
+ "unknown revision: #{super}"
29
+ end
30
+ end
31
+ class LocalChangesPresent < BraidError
32
+ def message
33
+ "local changes are present"
34
+ end
35
+ end
36
+
37
+ # The command proxy is meant to encapsulate commands such as git, git-svn and svn, that work with subcommands.
38
+ class Proxy
39
+ include Singleton
40
+
41
+ def self.command; name.split('::').last.downcase; end # hax!
42
+
43
+ def version
44
+ status, out, err = exec!("#{self.class.command} --version")
45
+ out.sub(/^.* version/, "").strip
46
+ end
47
+
48
+ def require_version(required)
49
+ required = required.split(".")
50
+ actual = version.split(".")
51
+
52
+ actual.each_with_index do |actual_piece, idx|
53
+ required_piece = required[idx]
54
+
55
+ return true unless required_piece
56
+
57
+ case (actual_piece <=> required_piece)
58
+ when -1
59
+ return false
60
+ when 1
61
+ return true
62
+ when 0
63
+ next
64
+ end
65
+ end
66
+
67
+ return actual.length >= required.length
68
+ end
69
+
70
+ def require_version!(required)
71
+ require_version(required) || raise(VersionTooLow.new(self.class.command, version))
72
+ end
73
+
74
+ private
75
+ def command(name)
76
+ # stub
77
+ name
78
+ end
79
+
80
+ def invoke(arg, *args)
81
+ exec!("#{command(arg)} #{args.join(' ')}".strip)[1].strip # return stdout
82
+ end
83
+
84
+ def method_missing(name, *args)
85
+ invoke(name, *args)
86
+ end
87
+
88
+ def exec(cmd)
89
+ cmd.strip!
90
+
91
+ previous_lang = ENV['LANG']
92
+ ENV['LANG'] = 'C'
93
+
94
+ out, err = nil
95
+ status = Open4.popen4(cmd) do |pid, stdin, stdout, stderr|
96
+ out = stdout.read
97
+ err = stderr.read
98
+ end.exitstatus
99
+ [status, out, err]
100
+
101
+ ensure
102
+ ENV['LANG'] = previous_lang
103
+ end
104
+
105
+ def exec!(cmd)
106
+ status, out, err = exec(cmd)
107
+ raise ShellExecutionError, err unless status == 0
108
+ [status, out, err]
109
+ end
110
+ end
111
+
112
+ class Git < Proxy
113
+ def commit(message, *args)
114
+ status, out, err = exec("git commit -m #{message.inspect} --no-verify #{args.join(' ')}")
115
+
116
+ if status == 0
117
+ true
118
+ elsif out.match(/nothing.* to commit/)
119
+ false
120
+ else
121
+ raise ShellExecutionError, err
122
+ end
123
+ end
124
+
125
+ def fetch(remote)
126
+ # open4 messes with the pipes of index-pack
127
+ system("git fetch -n #{remote} &> /dev/null")
128
+ raise ShellExecutionError, "could not fetch" unless $? == 0
129
+ true
130
+ end
131
+
132
+ def checkout(treeish)
133
+ # TODO debug
134
+ msg "Checking out '#{treeish}'."
135
+ invoke(:checkout, treeish)
136
+ true
137
+ end
138
+
139
+ # Returns the base commit or nil.
140
+ def merge_base(target, source)
141
+ invoke(:merge_base, target, source)
142
+ rescue ShellExecutionError
143
+ nil
144
+ end
145
+
146
+ def rev_parse(opt)
147
+ invoke(:rev_parse, opt)
148
+ rescue ShellExecutionError
149
+ raise UnknownRevision, opt
150
+ end
151
+
152
+ # Implies tracking.
153
+ def remote_add(remote, path, branch)
154
+ invoke(:remote, "add", "-t #{branch} -m #{branch}", remote, path)
155
+ true
156
+ end
157
+
158
+ # Checks git and svn remotes.
159
+ def remote_exists?(remote)
160
+ # TODO clean up and maybe return more information
161
+ !!File.readlines(".git/config").find { |line| line =~ /^\[(svn-)?remote "#{Regexp.escape(remote)}"\]/ }
162
+ end
163
+
164
+ def reset_hard(target)
165
+ invoke(:reset, "--hard", target)
166
+ true
167
+ end
168
+
169
+ # Implies no commit.
170
+ def merge_ours(opt)
171
+ invoke(:merge, "-s ours --no-commit", opt)
172
+ true
173
+ end
174
+
175
+ # Implies no commit.
176
+ def merge_subtree(opt)
177
+ # TODO which options are needed?
178
+ invoke(:merge, "-s subtree --no-commit --no-ff", opt)
179
+ true
180
+ end
181
+
182
+ def read_tree(treeish, prefix)
183
+ invoke(:read_tree, "--prefix=#{prefix}/ -u", treeish)
184
+ true
185
+ end
186
+
187
+ def rm_r(path)
188
+ invoke(:rm, "-r", path)
189
+ true
190
+ end
191
+
192
+ def tree_hash(path, treeish = "HEAD")
193
+ out = invoke(:ls_tree, treeish, "-d", path)
194
+ out.split[2]
195
+ end
196
+
197
+ def diff_tree(src_tree, dst_tree, prefix = nil)
198
+ cmd = "git diff-tree -p --binary #{src_tree} #{dst_tree}"
199
+ cmd << " --src-prefix=a/#{prefix}/ --dst-prefix=b/#{prefix}/" if prefix
200
+ status, out, err = exec!(cmd)
201
+ out
202
+ end
203
+
204
+ def status_clean?
205
+ status, out, err = exec("git status")
206
+ !out.split("\n").grep(/nothing to commit/).empty?
207
+ end
208
+
209
+ def ensure_clean!
210
+ status_clean? || raise(LocalChangesPresent)
211
+ end
212
+
213
+ def head
214
+ rev_parse("HEAD")
215
+ end
216
+
217
+ def branch
218
+ status, out, err = exec!("git branch | grep '*'")
219
+ out[2..-1]
220
+ end
221
+
222
+ def apply(diff, *args)
223
+ err = nil
224
+ status = Open4.popen4("git apply --index --whitespace=nowarn #{args.join(' ')} -") do |pid, stdin, stdout, stderr|
225
+ stdin.puts(diff)
226
+ stdin.close
227
+
228
+ err = stderr.read
229
+ end.exitstatus
230
+ raise ShellExecutionError, err unless status == 0
231
+ true
232
+ end
233
+
234
+ private
235
+ def command(name)
236
+ "#{self.class.command} #{name.to_s.gsub('_', '-')}"
237
+ end
238
+ end
239
+
240
+ class GitSvn < Proxy
241
+ def self.command; "git svn"; end
242
+
243
+ def commit_hash(remote, revision)
244
+ out = invoke(:log, "--show-commit --oneline", "-r #{revision}", remote)
245
+ part = out.to_s.split(" | ")[1]
246
+ raise UnknownRevision, "r#{revision}" unless part
247
+ Git.instance.rev_parse(part) # FIXME ugly ugly ugly
248
+ end
249
+
250
+ def fetch(remote)
251
+ # open4 messes with the pipes of index-pack
252
+ system("git svn fetch #{remote} &> /dev/null")
253
+ raise ShellExecutionError, "could not fetch" unless $? == 0
254
+ true
255
+ end
256
+
257
+ def init(remote, path)
258
+ invoke(:init, "-R", remote, "--id=#{remote}", path)
259
+ true
260
+ end
261
+
262
+ private
263
+ def command(name)
264
+ "#{self.class.command} #{name}"
265
+ end
266
+ end
267
+
268
+ class Svn < Proxy
269
+ def clean_revision(revision)
270
+ revision.to_i if revision
271
+ end
272
+
273
+ def head_revision(path)
274
+ # not using svn info because it's retarded and doesn't show the actual last changed rev for the url
275
+ # git svn has no clue on how to get the actual HEAD revision number on it's own
276
+ status, out, err = exec!("svn log -q --limit 1 #{path}")
277
+ out.split(/\n/).find { |x| x.match /^r\d+/ }.split(" | ")[0][1..-1].to_i
278
+ end
279
+ end
280
+
281
+ module VersionControl
282
+ def git
283
+ Git.instance
284
+ end
285
+
286
+ def git_svn
287
+ GitSvn.instance
288
+ end
289
+
290
+ def svn
291
+ Svn.instance
292
+ end
293
+ end
294
+ end
295
+ end
296
+
297
+
data/lib/braid.rb ADDED
@@ -0,0 +1,23 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
3
+ module Braid
4
+ VERSION = "0.4.9"
5
+
6
+ CONFIG_FILE = ".braids"
7
+ REQUIRED_GIT_VERSION = "1.5.4.5"
8
+
9
+ class BraidError < StandardError
10
+ def message
11
+ value = super
12
+ value if value != self.class.name
13
+ end
14
+ end
15
+ end
16
+
17
+ require 'braid/operations'
18
+ require 'braid/mirror'
19
+ require 'braid/config'
20
+ require 'braid/command'
21
+ Dir[File.dirname(__FILE__) + '/braid/commands/*'].each do |file|
22
+ require file
23
+ 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