dreamcat4-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.
- data/LICENSE +20 -0
- data/README.textile +120 -0
- data/Rakefile +17 -0
- data/bin/braid +249 -0
- data/braid.gemspec +25 -0
- data/lib/braid/command.rb +131 -0
- data/lib/braid/commands/add.rb +48 -0
- data/lib/braid/commands/diff.rb +20 -0
- data/lib/braid/commands/remove.rb +40 -0
- data/lib/braid/commands/setup.rb +38 -0
- data/lib/braid/commands/update.rb +107 -0
- data/lib/braid/config.rb +101 -0
- data/lib/braid/mirror.rb +180 -0
- data/lib/braid/operations.rb +456 -0
- data/lib/braid.rb +29 -0
- data/test/braid_test.rb +7 -0
- data/test/config_test.rb +62 -0
- data/test/fixtures/shiny/README +3 -0
- data/test/fixtures/skit1/layouts/layout.liquid +219 -0
- data/test/fixtures/skit1/preview.png +0 -0
- data/test/fixtures/skit1.1/layouts/layout.liquid +219 -0
- data/test/fixtures/skit1.2/layouts/layout.liquid +221 -0
- data/test/integration/adding_test.rb +80 -0
- data/test/integration/updating_test.rb +87 -0
- data/test/integration_helper.rb +70 -0
- data/test/mirror_test.rb +110 -0
- data/test/operations_test.rb +66 -0
- data/test/test_helper.rb +15 -0
- metadata +104 -0
@@ -0,0 +1,38 @@
|
|
1
|
+
module Braid
|
2
|
+
module Commands
|
3
|
+
class Setup < Command
|
4
|
+
def run(path = nil)
|
5
|
+
path ? setup_one(path) : setup_all
|
6
|
+
end
|
7
|
+
|
8
|
+
protected
|
9
|
+
def setup_all
|
10
|
+
msg "Setting up all mirrors."
|
11
|
+
config.mirrors.each do |path|
|
12
|
+
setup_one(path)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def setup_one(path)
|
17
|
+
mirror = config.get!(path)
|
18
|
+
|
19
|
+
if mirror.type == "git-clone"
|
20
|
+
return
|
21
|
+
end
|
22
|
+
|
23
|
+
if git.remote_url(mirror.remote)
|
24
|
+
msg "Setup: Mirror '#{mirror.path}' already has a remote. Reusing it." if verbose?
|
25
|
+
return
|
26
|
+
end
|
27
|
+
|
28
|
+
msg "Setup: Creating remote for '#{mirror.path}'."
|
29
|
+
unless mirror.type == "svn"
|
30
|
+
url = use_local_cache? ? git_cache.path(mirror.url) : mirror.url
|
31
|
+
git.remote_add(mirror.remote, url, mirror.branch)
|
32
|
+
else
|
33
|
+
git_svn.init(mirror.remote, mirror.url)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
module Braid
|
2
|
+
module Commands
|
3
|
+
class Update < Command
|
4
|
+
def run(path, options = {})
|
5
|
+
bail_on_local_changes!
|
6
|
+
|
7
|
+
with_reset_on_error do
|
8
|
+
path ? update_one(path, options) : update_all(options)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
protected
|
13
|
+
def update_all(options = {})
|
14
|
+
options.reject! { |k,v| %w(revision head).include?(k) }
|
15
|
+
msg "Updating all mirrors."
|
16
|
+
config.mirrors.each do |path|
|
17
|
+
update_one(path, options)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def update_one(path, options = {})
|
22
|
+
mirror = config.get!(path)
|
23
|
+
|
24
|
+
revision_message = options["revision"] ? " to #{display_revision(mirror, options["revision"])}" : ""
|
25
|
+
msg "Updating mirror '#{mirror.path}'#{revision_message}."
|
26
|
+
|
27
|
+
if mirror.type == "git-clone"
|
28
|
+
mirror.rspec_git.update options["revision"]
|
29
|
+
else
|
30
|
+
|
31
|
+
# check options for lock modification
|
32
|
+
if mirror.locked?
|
33
|
+
if options["head"]
|
34
|
+
msg "Unlocking mirror '#{mirror.path}'." if verbose?
|
35
|
+
mirror.lock = nil
|
36
|
+
elsif !options["revision"]
|
37
|
+
msg "Mirror '#{mirror.path}' is locked to #{display_revision(mirror, mirror.lock)}. Use --head to force."
|
38
|
+
return
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
setup_remote(mirror)
|
43
|
+
msg "Fetching new commits for '#{mirror.path}'." if verbose?
|
44
|
+
mirror.fetch
|
45
|
+
|
46
|
+
new_revision = validate_new_revision(mirror, options["revision"])
|
47
|
+
target_revision = determine_target_revision(mirror, new_revision)
|
48
|
+
|
49
|
+
if mirror.merged?(target_revision)
|
50
|
+
msg "Mirror '#{mirror.path}' is already up to date."
|
51
|
+
return
|
52
|
+
end
|
53
|
+
|
54
|
+
if mirror.squashed?
|
55
|
+
diff = mirror.diff
|
56
|
+
base_revision = mirror.base_revision
|
57
|
+
end
|
58
|
+
|
59
|
+
mirror.revision = new_revision
|
60
|
+
mirror.lock = new_revision if options["revision"]
|
61
|
+
|
62
|
+
msg "Merging in mirror '#{mirror.path}'." if verbose?
|
63
|
+
begin
|
64
|
+
if mirror.squashed?
|
65
|
+
local_hash = git.rev_parse("HEAD")
|
66
|
+
if diff
|
67
|
+
base_hash = generate_tree_hash(mirror, base_revision)
|
68
|
+
else
|
69
|
+
base_hash = local_hash
|
70
|
+
end
|
71
|
+
remote_hash = generate_tree_hash(mirror, target_revision)
|
72
|
+
ENV["GITHEAD_#{local_hash}"] = "HEAD"
|
73
|
+
ENV["GITHEAD_#{remote_hash}"] = target_revision
|
74
|
+
git.merge_recursive(base_hash, local_hash, remote_hash)
|
75
|
+
else
|
76
|
+
git.merge_subtree(target_revision)
|
77
|
+
end
|
78
|
+
rescue Operations::MergeError => error
|
79
|
+
msg "Caught merge error. Breaking."
|
80
|
+
end
|
81
|
+
|
82
|
+
config.update(mirror)
|
83
|
+
add_config_file
|
84
|
+
|
85
|
+
commit_message = "Updated mirror '#{mirror.path}' to #{display_revision(mirror)}"
|
86
|
+
|
87
|
+
if error
|
88
|
+
File.open(".git/MERGE_MSG", 'w') { |f| f.puts(commit_message) }
|
89
|
+
return
|
90
|
+
end
|
91
|
+
|
92
|
+
git.commit(commit_message)
|
93
|
+
msg commit_message
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def generate_tree_hash(mirror, revision)
|
98
|
+
git.rm_r(mirror.path)
|
99
|
+
git.read_tree_prefix(revision, mirror.path)
|
100
|
+
success = git.commit("Temporary commit for mirror '#{mirror.path}'")
|
101
|
+
hash = git.rev_parse("HEAD")
|
102
|
+
git.reset_hard("HEAD^") if success
|
103
|
+
hash
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
data/lib/braid/config.rb
ADDED
@@ -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
|
data/lib/braid/mirror.rb
ADDED
@@ -0,0 +1,180 @@
|
|
1
|
+
require 'rspec_git.rb'
|
2
|
+
|
3
|
+
module Braid
|
4
|
+
class Mirror
|
5
|
+
TYPES = %w(git svn git-clone)
|
6
|
+
ATTRIBUTES = %w(url remote type branch squashed revision lock)
|
7
|
+
|
8
|
+
class UnknownType < BraidError
|
9
|
+
def message
|
10
|
+
"unknown type: #{super}"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
class CannotGuessType < BraidError
|
14
|
+
def message
|
15
|
+
"cannot guess type: #{super}"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
class PathRequired < BraidError
|
19
|
+
def message
|
20
|
+
"path is required"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
include Operations::VersionControl
|
25
|
+
|
26
|
+
attr_reader :path, :attributes, :rspec_git
|
27
|
+
|
28
|
+
def initialize(path, attributes = {})
|
29
|
+
@path = path.sub(/\/$/, '')
|
30
|
+
@attributes = attributes
|
31
|
+
@rspec_git = RSpec::Git.new File.basename(@path) @path attributes["url"]
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.new_from_options(url, options = {})
|
35
|
+
url = url.sub(/\/$/, '')
|
36
|
+
|
37
|
+
branch = options["branch"] || "master"
|
38
|
+
|
39
|
+
if type = options["type"] || extract_type_from_url(url)
|
40
|
+
raise UnknownType, type unless TYPES.include?(type)
|
41
|
+
else
|
42
|
+
raise CannotGuessType, url
|
43
|
+
end
|
44
|
+
|
45
|
+
unless path = options["path"] || extract_path_from_url(url)
|
46
|
+
raise PathRequired
|
47
|
+
end
|
48
|
+
|
49
|
+
if options["rails_plugin"] && ! path =~ /vendor\/plugins.*/
|
50
|
+
path = "vendor/plugins/#{path}"
|
51
|
+
end
|
52
|
+
|
53
|
+
if options["rails_gem"] && ! path =~ /vendor\/gems.*/
|
54
|
+
path = "vendor/gems/#{path}"
|
55
|
+
end
|
56
|
+
|
57
|
+
remote = "braid/#{path}".gsub("_", '-') # stupid git svn changes all _ to ., weird
|
58
|
+
squashed = !options["full"]
|
59
|
+
branch = nil if type == "svn"
|
60
|
+
|
61
|
+
attributes = { "url" => url, "remote" => remote, "type" => type, "branch" => branch, "squashed" => squashed }
|
62
|
+
self.new(path, attributes)
|
63
|
+
end
|
64
|
+
|
65
|
+
def ==(comparison)
|
66
|
+
path == comparison.path && attributes == comparison.attributes
|
67
|
+
end
|
68
|
+
|
69
|
+
def type
|
70
|
+
# override Object#type
|
71
|
+
attributes["type"]
|
72
|
+
end
|
73
|
+
|
74
|
+
def locked?
|
75
|
+
!!lock
|
76
|
+
end
|
77
|
+
|
78
|
+
def squashed?
|
79
|
+
!!squashed
|
80
|
+
end
|
81
|
+
|
82
|
+
def merged?(commit)
|
83
|
+
# tip from spearce in #git:
|
84
|
+
# `test z$(git merge-base A B) = z$(git rev-parse --verify A)`
|
85
|
+
commit = git.rev_parse(commit)
|
86
|
+
if squashed?
|
87
|
+
!!base_revision && git.merge_base(commit, base_revision) == commit
|
88
|
+
else
|
89
|
+
git.merge_base(commit, "HEAD") == commit
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def diff
|
94
|
+
remote_hash = git.rev_parse("#{base_revision}:")
|
95
|
+
local_hash = git.tree_hash(path)
|
96
|
+
remote_hash != local_hash ? git.diff_tree(remote_hash, local_hash, path) : ""
|
97
|
+
end
|
98
|
+
|
99
|
+
def fetch
|
100
|
+
unless type == "svn"
|
101
|
+
git_cache.fetch(url) if cached?
|
102
|
+
git.fetch(remote)
|
103
|
+
else
|
104
|
+
git_svn.fetch(remote)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def cached?
|
109
|
+
git.remote_url(remote) == git_cache.path(url)
|
110
|
+
end
|
111
|
+
|
112
|
+
def base_revision
|
113
|
+
if revision
|
114
|
+
unless type == "svn"
|
115
|
+
git.rev_parse(revision)
|
116
|
+
else
|
117
|
+
git_svn.commit_hash(remote, revision)
|
118
|
+
end
|
119
|
+
else
|
120
|
+
inferred_revision
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
125
|
+
def method_missing(name, *args)
|
126
|
+
if ATTRIBUTES.find { |attribute| name.to_s =~ /^(#{attribute})(=)?$/ }
|
127
|
+
unless $2
|
128
|
+
attributes[$1]
|
129
|
+
else
|
130
|
+
attributes[$1] = args[0]
|
131
|
+
end
|
132
|
+
else
|
133
|
+
raise NameError, "unknown attribute `#{name}'"
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def inferred_revision
|
138
|
+
local_commits = git.rev_list("HEAD", "-- #{path}").split("\n")
|
139
|
+
remote_hashes = git.rev_list("--pretty=format:\"%T\"", remote).split("commit ").map do |chunk|
|
140
|
+
chunk.split("\n", 2).map { |value| value.strip }
|
141
|
+
end
|
142
|
+
hash = nil
|
143
|
+
local_commits.each do |local_commit|
|
144
|
+
local_tree = git.tree_hash(path, local_commit)
|
145
|
+
if match = remote_hashes.find { |_, remote_tree| local_tree == remote_tree }
|
146
|
+
hash = match[0]
|
147
|
+
break
|
148
|
+
end
|
149
|
+
end
|
150
|
+
hash
|
151
|
+
end
|
152
|
+
|
153
|
+
def self.extract_type_from_url(url)
|
154
|
+
return nil unless url
|
155
|
+
url.sub!(/\/$/, '')
|
156
|
+
|
157
|
+
# check for git:// and svn:// URLs
|
158
|
+
url_scheme = url.split(":").first
|
159
|
+
return url_scheme if TYPES.include?(url_scheme)
|
160
|
+
|
161
|
+
return "svn" if url[-6..-1] == "/trunk"
|
162
|
+
return "git-clone" if url[-4..-1] == ".git"
|
163
|
+
end
|
164
|
+
|
165
|
+
def self.extract_path_from_url(url)
|
166
|
+
return nil unless url
|
167
|
+
name = File.basename(url)
|
168
|
+
|
169
|
+
if File.extname(name) == ".git"
|
170
|
+
# strip .git
|
171
|
+
name[0..-5]
|
172
|
+
elsif name == "trunk"
|
173
|
+
# use parent
|
174
|
+
File.basename(File.dirname(url))
|
175
|
+
else
|
176
|
+
name
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|