realityforge-braid 0.7.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +12 -0
- data/Gemfile +6 -0
- data/LICENSE +20 -0
- data/README.md +115 -0
- data/Rakefile +20 -0
- data/bin/braid +263 -0
- data/braid.gemspec +33 -0
- data/lib/braid/command.rb +136 -0
- data/lib/braid/commands/add.rb +40 -0
- data/lib/braid/commands/diff.rb +13 -0
- data/lib/braid/commands/list.rb +32 -0
- data/lib/braid/commands/push.rb +48 -0
- data/lib/braid/commands/remove.rb +32 -0
- data/lib/braid/commands/setup.rb +40 -0
- data/lib/braid/commands/update.rb +102 -0
- data/lib/braid/config.rb +106 -0
- data/lib/braid/mirror.rb +186 -0
- data/lib/braid/operations.rb +423 -0
- data/lib/braid/version.rb +3 -0
- data/lib/braid.rb +48 -0
- data/lib/core_ext.rb +13 -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 +155 -0
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'tmpdir'
|
3
|
+
|
4
|
+
module Braid
|
5
|
+
module Commands
|
6
|
+
class Push < Command
|
7
|
+
def run(path, options = {})
|
8
|
+
mirror = config.get!(path)
|
9
|
+
|
10
|
+
#mirror.fetch
|
11
|
+
|
12
|
+
base_revision = git.rev_parse(mirror.remote)
|
13
|
+
unless mirror.merged?(base_revision)
|
14
|
+
msg "Mirror is not up to date. Stopping."
|
15
|
+
return
|
16
|
+
end
|
17
|
+
|
18
|
+
diff = mirror.diff
|
19
|
+
if diff.empty?
|
20
|
+
msg "No local changes found. Stopping."
|
21
|
+
return
|
22
|
+
end
|
23
|
+
|
24
|
+
clone_dir = Dir.tmpdir + "/braid_push.#{$$}"
|
25
|
+
Dir.mkdir(clone_dir)
|
26
|
+
source_dir = Dir.pwd
|
27
|
+
remote_url = git.remote_url(mirror.remote)
|
28
|
+
if remote_url == mirror.cached_url
|
29
|
+
remote_url = mirror.url
|
30
|
+
elsif File.directory?(remote_url)
|
31
|
+
remote_url = File.expand_path(remote_url)
|
32
|
+
end
|
33
|
+
Dir.chdir(clone_dir) do
|
34
|
+
msg "Cloning mirror with local changes."
|
35
|
+
git.init
|
36
|
+
git.fetch(source_dir)
|
37
|
+
git.fetch(remote_url, "+refs/heads/#{mirror.branch}")
|
38
|
+
git.checkout(base_revision)
|
39
|
+
git.apply(diff)
|
40
|
+
system("git commit -v")
|
41
|
+
msg "Pushing changes to remote."
|
42
|
+
git.push(remote_url, "HEAD:#{mirror.branch}")
|
43
|
+
end
|
44
|
+
FileUtils.rm_r(clone_dir)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Braid
|
2
|
+
module Commands
|
3
|
+
class Remove < Command
|
4
|
+
def run(path, options = {})
|
5
|
+
mirror = config.get!(path)
|
6
|
+
|
7
|
+
bail_on_local_changes!
|
8
|
+
|
9
|
+
with_reset_on_error do
|
10
|
+
msg "Removing mirror from '#{mirror.path}'."
|
11
|
+
|
12
|
+
git.rm_r(mirror.path)
|
13
|
+
|
14
|
+
config.remove(mirror)
|
15
|
+
add_config_file
|
16
|
+
|
17
|
+
if options[:keep]
|
18
|
+
msg "Not removing remote '#{mirror.remote}'" if verbose?
|
19
|
+
elsif git.remote_url(mirror.remote)
|
20
|
+
msg "Removed remote '#{mirror.path}'" if verbose?
|
21
|
+
git.remote_rm mirror.remote
|
22
|
+
else
|
23
|
+
msg "Remote '#{mirror.remote}' not found, nothing to cleanup" if verbose?
|
24
|
+
end
|
25
|
+
|
26
|
+
git.commit("Remove mirror '#{mirror.path}'")
|
27
|
+
msg "Removed mirror." if verbose?
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,40 @@
|
|
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
|
+
|
10
|
+
def setup_all
|
11
|
+
msg "Setting up all mirrors."
|
12
|
+
config.mirrors.each do |path|
|
13
|
+
setup_one(path)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def setup_one(path)
|
18
|
+
mirror = config.get!(path)
|
19
|
+
|
20
|
+
if git.remote_url(mirror.remote)
|
21
|
+
if force?
|
22
|
+
msg "Setup: Mirror '#{mirror.path}' already has a remote. Replacing it (force)" if verbose?
|
23
|
+
git.remote_rm(mirror.remote)
|
24
|
+
else
|
25
|
+
msg "Setup: Mirror '#{mirror.path}' already has a remote. Reusing it." if verbose?
|
26
|
+
return
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
msg "Setup: Creating remote for '#{mirror.path}'."
|
31
|
+
unless mirror.type == "svn"
|
32
|
+
url = use_local_cache? ? git_cache.path(mirror.url) : mirror.url
|
33
|
+
git.remote_add(mirror.remote, url, mirror.branch)
|
34
|
+
else
|
35
|
+
git_svn.init(mirror.remote, mirror.url)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,102 @@
|
|
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
|
+
|
14
|
+
def update_all(options = {})
|
15
|
+
options.reject! { |k, v| %w(revision head).include?(k) }
|
16
|
+
msg "Updating all mirrors."
|
17
|
+
config.mirrors.each do |path|
|
18
|
+
update_one(path, options)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def update_one(path, options = {})
|
23
|
+
mirror = config.get!(path)
|
24
|
+
|
25
|
+
revision_message = options["revision"] ? " to #{display_revision(mirror, options["revision"])}" : ""
|
26
|
+
msg "Updating mirror '#{mirror.path}'#{revision_message}."
|
27
|
+
|
28
|
+
# check options for lock modification
|
29
|
+
if mirror.locked?
|
30
|
+
if options["head"]
|
31
|
+
msg "Unlocking mirror '#{mirror.path}'." if verbose?
|
32
|
+
mirror.lock = nil
|
33
|
+
elsif !options["revision"]
|
34
|
+
msg "Mirror '#{mirror.path}' is locked to #{display_revision(mirror, mirror.lock)}. Use --head to force."
|
35
|
+
return
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
setup_remote(mirror)
|
40
|
+
msg "Fetching new commits for '#{mirror.path}'." if verbose?
|
41
|
+
mirror.fetch
|
42
|
+
|
43
|
+
new_revision = validate_new_revision(mirror, options["revision"])
|
44
|
+
target_revision = determine_target_revision(mirror, new_revision)
|
45
|
+
|
46
|
+
if mirror.merged?(target_revision)
|
47
|
+
msg "Mirror '#{mirror.path}' is already up to date."
|
48
|
+
return
|
49
|
+
end
|
50
|
+
|
51
|
+
if mirror.squashed?
|
52
|
+
diff = mirror.diff
|
53
|
+
base_revision = mirror.base_revision
|
54
|
+
end
|
55
|
+
|
56
|
+
mirror.revision = new_revision
|
57
|
+
mirror.lock = new_revision if options["revision"]
|
58
|
+
|
59
|
+
msg "Merging in mirror '#{mirror.path}'." if verbose?
|
60
|
+
begin
|
61
|
+
if mirror.squashed?
|
62
|
+
local_hash = git.rev_parse("HEAD")
|
63
|
+
if !diff.empty?
|
64
|
+
base_hash = generate_tree_hash(mirror, base_revision)
|
65
|
+
else
|
66
|
+
base_hash = local_hash
|
67
|
+
end
|
68
|
+
remote_hash = generate_tree_hash(mirror, target_revision)
|
69
|
+
ENV["GITHEAD_#{local_hash}"] = "HEAD"
|
70
|
+
ENV["GITHEAD_#{remote_hash}"] = target_revision
|
71
|
+
git.merge_recursive(base_hash, local_hash, remote_hash)
|
72
|
+
else
|
73
|
+
git.merge_subtree(target_revision)
|
74
|
+
end
|
75
|
+
rescue Operations::MergeError => error
|
76
|
+
msg "Caught merge error. Breaking."
|
77
|
+
end
|
78
|
+
|
79
|
+
config.update(mirror)
|
80
|
+
add_config_file
|
81
|
+
|
82
|
+
commit_message = "Update mirror '#{mirror.path}' to #{display_revision(mirror)}"
|
83
|
+
if error
|
84
|
+
File.open(".git/MERGE_MSG", 'w') { |f| f.puts(commit_message) }
|
85
|
+
return
|
86
|
+
end
|
87
|
+
|
88
|
+
git.commit(commit_message)
|
89
|
+
msg "Updated mirror to #{display_revision(mirror)}."
|
90
|
+
end
|
91
|
+
|
92
|
+
def generate_tree_hash(mirror, revision)
|
93
|
+
git.rm_r(mirror.path)
|
94
|
+
git.read_tree_prefix(revision, mirror.path)
|
95
|
+
success = git.commit("Temporary commit for mirror '#{mirror.path}'")
|
96
|
+
hash = git.rev_parse("HEAD")
|
97
|
+
git.reset_hard("HEAD^") if success
|
98
|
+
hash
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
data/lib/braid/config.rb
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
# psych throws such wonderful errors as:
|
4
|
+
# `@vendor/rails' is not allowed as an instance variable name (NameError)
|
5
|
+
YAML::ENGINE.yamler = 'syck' if RUBY_VERSION >= '1.9.1'
|
6
|
+
|
7
|
+
require 'yaml/store'
|
8
|
+
|
9
|
+
module Braid
|
10
|
+
class Config
|
11
|
+
class PathAlreadyInUse < BraidError
|
12
|
+
def message
|
13
|
+
"path already in use: #{super}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
class MirrorDoesNotExist < BraidError
|
17
|
+
def message
|
18
|
+
"mirror does not exist: #{super}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(config_file = CONFIG_FILE)
|
23
|
+
@db = YAML::Store.new(config_file)
|
24
|
+
end
|
25
|
+
|
26
|
+
def add_from_options(url, options)
|
27
|
+
mirror = Mirror.new_from_options(url, options)
|
28
|
+
|
29
|
+
add(mirror)
|
30
|
+
mirror
|
31
|
+
end
|
32
|
+
|
33
|
+
def mirrors
|
34
|
+
@db.transaction(true) do
|
35
|
+
@db.roots
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def get(path)
|
40
|
+
@db.transaction(true) do
|
41
|
+
if attributes = @db[path.to_s.sub(/\/$/, '')]
|
42
|
+
Mirror.new(path, attributes)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def get!(path)
|
48
|
+
mirror = get(path)
|
49
|
+
raise MirrorDoesNotExist, path unless mirror
|
50
|
+
mirror
|
51
|
+
end
|
52
|
+
|
53
|
+
def add(mirror)
|
54
|
+
@db.transaction do
|
55
|
+
raise PathAlreadyInUse, mirror.path if @db[mirror.path]
|
56
|
+
write_mirror(mirror)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def remove(mirror)
|
61
|
+
@db.transaction do
|
62
|
+
@db.delete(mirror.path)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def update(mirror)
|
67
|
+
@db.transaction do
|
68
|
+
raise MirrorDoesNotExist, mirror.path unless @db[mirror.path]
|
69
|
+
@db.delete(mirror.path)
|
70
|
+
write_mirror(mirror)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def valid?
|
75
|
+
@db.transaction(true) do
|
76
|
+
!@db.roots.any? do |path|
|
77
|
+
@db[path]["url"].nil?
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def migrate!
|
83
|
+
@db.transaction do
|
84
|
+
@db.roots.each do |path|
|
85
|
+
attributes = @db[path]
|
86
|
+
if attributes["local_branch"]
|
87
|
+
attributes["url"] = attributes.delete("remote")
|
88
|
+
attributes["remote"] = attributes.delete("local_branch")
|
89
|
+
attributes["squashed"] = attributes.delete("squash")
|
90
|
+
attributes["lock"] = attributes["revision"] # so far this has always been true
|
91
|
+
end
|
92
|
+
@db[path] = clean_attributes(attributes)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
def write_mirror(mirror)
|
99
|
+
@db[mirror.path] = clean_attributes(mirror.attributes)
|
100
|
+
end
|
101
|
+
|
102
|
+
def clean_attributes(hash)
|
103
|
+
hash.reject { |k, v| v.nil? }
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
data/lib/braid/mirror.rb
ADDED
@@ -0,0 +1,186 @@
|
|
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 = "#{branch}/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) : ""
|
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) == cached_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
|
+
def cached_url
|
118
|
+
git_cache.path(url)
|
119
|
+
end
|
120
|
+
|
121
|
+
def remote
|
122
|
+
if (attributes["remote"] && attributes["remote"] =~ /^braid\//)
|
123
|
+
attributes["remote"] = "#{branch}/#{attributes["remote"]}"
|
124
|
+
else
|
125
|
+
attributes["remote"]
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
private
|
130
|
+
|
131
|
+
def method_missing(name, *args)
|
132
|
+
if ATTRIBUTES.find { |attribute| name.to_s =~ /^(#{attribute})(=)?$/ }
|
133
|
+
unless $2
|
134
|
+
attributes[$1]
|
135
|
+
else
|
136
|
+
attributes[$1] = args[0]
|
137
|
+
end
|
138
|
+
else
|
139
|
+
raise NameError, "unknown attribute `#{name}'"
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def inferred_revision
|
144
|
+
local_commits = git.rev_list("HEAD", "-- #{path}").split("\n")
|
145
|
+
remote_hashes = git.rev_list("--pretty=format:\"%T\"", remote).split("commit ").map do |chunk|
|
146
|
+
chunk.split("\n", 2).map { |value| value.strip }
|
147
|
+
end
|
148
|
+
hash = nil
|
149
|
+
local_commits.each do |local_commit|
|
150
|
+
local_tree = git.tree_hash(path, local_commit)
|
151
|
+
if match = remote_hashes.find { |_, remote_tree| local_tree == remote_tree }
|
152
|
+
hash = match[0]
|
153
|
+
break
|
154
|
+
end
|
155
|
+
end
|
156
|
+
hash
|
157
|
+
end
|
158
|
+
|
159
|
+
def self.extract_type_from_url(url)
|
160
|
+
return nil unless url
|
161
|
+
url.sub!(/\/$/, '')
|
162
|
+
|
163
|
+
# check for git:// and svn:// URLs
|
164
|
+
url_scheme = url.split(":").first
|
165
|
+
return url_scheme if TYPES.include?(url_scheme)
|
166
|
+
|
167
|
+
return "svn" if url[-6..-1] == "/trunk"
|
168
|
+
return "git" if url[-4..-1] == ".git"
|
169
|
+
end
|
170
|
+
|
171
|
+
def self.extract_path_from_url(url)
|
172
|
+
return nil unless url
|
173
|
+
name = File.basename(url)
|
174
|
+
|
175
|
+
if File.extname(name) == ".git"
|
176
|
+
# strip .git
|
177
|
+
name[0..-5]
|
178
|
+
elsif name == "trunk"
|
179
|
+
# use parent
|
180
|
+
File.basename(File.dirname(url))
|
181
|
+
else
|
182
|
+
name
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|