realityforge-braid 0.7.2
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/.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
|