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