braid 0.5
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 +230 -0
- data/braid.gemspec +25 -0
- data/lib/braid.rb +29 -0
- data/lib/braid/command.rb +131 -0
- data/lib/braid/commands/add.rb +42 -0
- data/lib/braid/commands/diff.rb +13 -0
- data/lib/braid/commands/remove.rb +27 -0
- data/lib/braid/commands/setup.rb +34 -0
- data/lib/braid/commands/update.rb +102 -0
- data/lib/braid/config.rb +101 -0
- data/lib/braid/mirror.rb +173 -0
- data/lib/braid/operations.rb +385 -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.1/layouts/layout.liquid +219 -0
- data/test/fixtures/skit1.2/layouts/layout.liquid +221 -0
- data/test/fixtures/skit1/layouts/layout.liquid +219 -0
- data/test/fixtures/skit1/preview.png +0 -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,131 @@
|
|
1
|
+
module Braid
|
2
|
+
class Command
|
3
|
+
class InvalidRevision < BraidError
|
4
|
+
end
|
5
|
+
|
6
|
+
extend Operations::VersionControl
|
7
|
+
include Operations::VersionControl
|
8
|
+
|
9
|
+
def self.run(command, *args)
|
10
|
+
verify_git_version!
|
11
|
+
|
12
|
+
klass = Commands.const_get(command.to_s.capitalize)
|
13
|
+
klass.new.run(*args)
|
14
|
+
|
15
|
+
rescue BraidError => error
|
16
|
+
case error
|
17
|
+
when Operations::ShellExecutionError
|
18
|
+
msg "Shell error: #{error.message}"
|
19
|
+
else
|
20
|
+
msg "Error: #{error.message}"
|
21
|
+
end
|
22
|
+
exit(1)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.msg(str)
|
26
|
+
puts "Braid: #{str}"
|
27
|
+
end
|
28
|
+
|
29
|
+
def msg(str)
|
30
|
+
self.class.msg(str)
|
31
|
+
end
|
32
|
+
|
33
|
+
def config
|
34
|
+
@config ||= load_and_migrate_config
|
35
|
+
end
|
36
|
+
|
37
|
+
def verbose?
|
38
|
+
Braid.verbose
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
def setup_remote(mirror)
|
43
|
+
Command.run(:setup, mirror.path)
|
44
|
+
end
|
45
|
+
|
46
|
+
def use_local_cache?
|
47
|
+
Braid.use_local_cache
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.verify_git_version!
|
51
|
+
git.require_version!(REQUIRED_GIT_VERSION)
|
52
|
+
end
|
53
|
+
|
54
|
+
def bail_on_local_changes!
|
55
|
+
git.ensure_clean!
|
56
|
+
end
|
57
|
+
|
58
|
+
def with_reset_on_error
|
59
|
+
work_head = git.head
|
60
|
+
|
61
|
+
begin
|
62
|
+
yield
|
63
|
+
rescue => error
|
64
|
+
msg "Resetting to '#{work_head[0, 7]}'."
|
65
|
+
git.reset_hard(work_head)
|
66
|
+
raise error
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def load_and_migrate_config
|
71
|
+
config = Config.new
|
72
|
+
unless config.valid?
|
73
|
+
msg "Configuration is outdated. Migrating."
|
74
|
+
bail_on_local_changes!
|
75
|
+
config.migrate!
|
76
|
+
git.commit("Upgrade .braids", "-- .braids")
|
77
|
+
end
|
78
|
+
config
|
79
|
+
end
|
80
|
+
|
81
|
+
def add_config_file
|
82
|
+
git.add(CONFIG_FILE)
|
83
|
+
end
|
84
|
+
|
85
|
+
def display_revision(mirror, revision = nil)
|
86
|
+
revision ||= mirror.revision
|
87
|
+
mirror.type == "svn" ? "r#{revision}" : "'#{revision[0, 7]}'"
|
88
|
+
end
|
89
|
+
|
90
|
+
def validate_new_revision(mirror, new_revision)
|
91
|
+
unless new_revision
|
92
|
+
unless mirror.type == "svn"
|
93
|
+
return git.rev_parse(mirror.remote)
|
94
|
+
else
|
95
|
+
return svn.head_revision(mirror.url)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
unless mirror.type == "svn"
|
100
|
+
new_revision = git.rev_parse(new_revision)
|
101
|
+
else
|
102
|
+
new_revision = svn.clean_revision(new_revision)
|
103
|
+
end
|
104
|
+
old_revision = mirror.revision
|
105
|
+
|
106
|
+
if new_revision == old_revision
|
107
|
+
raise InvalidRevision, "mirror is already at requested revision"
|
108
|
+
end
|
109
|
+
|
110
|
+
if mirror.type == "svn"
|
111
|
+
if old_revision && new_revision < old_revision
|
112
|
+
raise InvalidRevision, "local revision is higher than request revision"
|
113
|
+
end
|
114
|
+
|
115
|
+
if svn.head_revision(mirror.url) < new_revision
|
116
|
+
raise InvalidRevision, "requested revision is higher than remote revision"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
new_revision
|
121
|
+
end
|
122
|
+
|
123
|
+
def determine_target_revision(mirror, new_revision)
|
124
|
+
unless mirror.type == "svn"
|
125
|
+
git.rev_parse(new_revision)
|
126
|
+
else
|
127
|
+
git_svn.commit_hash(mirror.remote, new_revision)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Braid
|
2
|
+
module Commands
|
3
|
+
class Add < Command
|
4
|
+
def run(url, options = {})
|
5
|
+
bail_on_local_changes!
|
6
|
+
|
7
|
+
with_reset_on_error do
|
8
|
+
mirror = config.add_from_options(url, options)
|
9
|
+
|
10
|
+
branch_message = (mirror.type == "svn" || mirror.branch == "master") ? "" : " branch '#{mirror.branch}'"
|
11
|
+
revision_message = options["revision"] ? " at #{display_revision(mirror, options["revision"])}" : ""
|
12
|
+
msg "Adding #{mirror.type} mirror of '#{mirror.url}'#{branch_message}#{revision_message}."
|
13
|
+
|
14
|
+
# these commands are explained in the subtree merge guide
|
15
|
+
# http://www.kernel.org/pub/software/scm/git/docs/howto/using-merge-subtree.html
|
16
|
+
|
17
|
+
setup_remote(mirror)
|
18
|
+
mirror.fetch
|
19
|
+
|
20
|
+
new_revision = validate_new_revision(mirror, options["revision"])
|
21
|
+
target_revision = determine_target_revision(mirror, new_revision)
|
22
|
+
|
23
|
+
unless mirror.squashed?
|
24
|
+
git.merge_ours(target_revision)
|
25
|
+
end
|
26
|
+
git.read_tree_prefix(target_revision, mirror.path)
|
27
|
+
|
28
|
+
mirror.revision = new_revision
|
29
|
+
mirror.lock = new_revision if options["revision"]
|
30
|
+
config.update(mirror)
|
31
|
+
add_config_file
|
32
|
+
|
33
|
+
commit_message = "Added mirror '#{mirror.path}' at #{display_revision(mirror)}"
|
34
|
+
|
35
|
+
git.commit(commit_message)
|
36
|
+
msg commit_message
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Braid
|
2
|
+
module Commands
|
3
|
+
class Remove < Command
|
4
|
+
def run(path)
|
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
|
+
# will need this in case we decide to remove the .git/config entry also
|
15
|
+
# setup_remote(mirror)
|
16
|
+
|
17
|
+
config.remove(mirror)
|
18
|
+
add_config_file
|
19
|
+
|
20
|
+
commit_message = "Removed mirror '#{mirror.path}'"
|
21
|
+
git.commit(commit_message)
|
22
|
+
msg commit_message
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,34 @@
|
|
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 git.remote_url(mirror.remote)
|
20
|
+
msg "Setup: Mirror '#{mirror.path}' already has a remote. Reusing it." if verbose?
|
21
|
+
return
|
22
|
+
end
|
23
|
+
|
24
|
+
msg "Setup: Creating remote for '#{mirror.path}'."
|
25
|
+
unless mirror.type == "svn"
|
26
|
+
url = use_local_cache? ? git_cache.path(mirror.url) : mirror.url
|
27
|
+
git.remote_add(mirror.remote, url, mirror.branch)
|
28
|
+
else
|
29
|
+
git_svn.init(mirror.remote, mirror.url)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
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
|
+
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
|
+
# check options for lock modification
|
28
|
+
if mirror.locked?
|
29
|
+
if options["head"]
|
30
|
+
msg "Unlocking mirror '#{mirror.path}'." if verbose?
|
31
|
+
mirror.lock = nil
|
32
|
+
elsif !options["revision"]
|
33
|
+
msg "Mirror '#{mirror.path}' is locked to #{display_revision(mirror, mirror.lock)}. Use --head to force."
|
34
|
+
return
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
setup_remote(mirror)
|
39
|
+
msg "Fetching new commits for '#{mirror.path}'." if verbose?
|
40
|
+
mirror.fetch
|
41
|
+
|
42
|
+
new_revision = validate_new_revision(mirror, options["revision"])
|
43
|
+
target_revision = determine_target_revision(mirror, new_revision)
|
44
|
+
|
45
|
+
if mirror.merged?(target_revision)
|
46
|
+
msg "Mirror '#{mirror.path}' is already up to date."
|
47
|
+
return
|
48
|
+
end
|
49
|
+
|
50
|
+
if mirror.squashed?
|
51
|
+
diff = mirror.diff
|
52
|
+
base_revision = mirror.base_revision
|
53
|
+
end
|
54
|
+
|
55
|
+
mirror.revision = new_revision
|
56
|
+
mirror.lock = new_revision if options["revision"]
|
57
|
+
|
58
|
+
msg "Merging in mirror '#{mirror.path}'." if verbose?
|
59
|
+
begin
|
60
|
+
if mirror.squashed?
|
61
|
+
local_hash = git.rev_parse("HEAD")
|
62
|
+
if diff
|
63
|
+
base_hash = generate_tree_hash(mirror, base_revision)
|
64
|
+
else
|
65
|
+
base_hash = local_hash
|
66
|
+
end
|
67
|
+
remote_hash = generate_tree_hash(mirror, target_revision)
|
68
|
+
ENV["GITHEAD_#{local_hash}"] = "HEAD"
|
69
|
+
ENV["GITHEAD_#{remote_hash}"] = target_revision
|
70
|
+
git.merge_recursive(base_hash, local_hash, remote_hash)
|
71
|
+
else
|
72
|
+
git.merge_subtree(target_revision)
|
73
|
+
end
|
74
|
+
rescue Operations::MergeError => error
|
75
|
+
msg "Caught merge error. Breaking."
|
76
|
+
end
|
77
|
+
|
78
|
+
config.update(mirror)
|
79
|
+
add_config_file
|
80
|
+
|
81
|
+
commit_message = "Updated mirror '#{mirror.path}' to #{display_revision(mirror)}"
|
82
|
+
|
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 commit_message
|
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,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
|