norbert-braid 0.4.9

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,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