mlightner-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.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/list.rb +32 -0
- data/lib/braid/commands/remove.rb +33 -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 +390 -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 +103 -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,32 @@
|
|
1
|
+
module Braid
|
2
|
+
module Commands
|
3
|
+
class List < Command
|
4
|
+
def run(path = nil, options = {})
|
5
|
+
with_reset_on_error do
|
6
|
+
path ? list_one(path, options) : list_all(options)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
protected
|
11
|
+
def list_all(options = {})
|
12
|
+
options.reject! { |k,v| %w(revision head).include?(k) }
|
13
|
+
print "\n"
|
14
|
+
msg "Listing all mirrors.\n=======================================================\n"
|
15
|
+
config.mirrors.each_with_index do |path, i|
|
16
|
+
mirror = config.get!(path)
|
17
|
+
print " #{i + 1}) #{path.to_s}"
|
18
|
+
print " [LOCKED]" if mirror.locked?
|
19
|
+
setup_remote(mirror)
|
20
|
+
msg "Fetching new commits for '#{mirror.path}'." if verbose?
|
21
|
+
mirror.fetch
|
22
|
+
new_revision = validate_new_revision(mirror, options["revision"])
|
23
|
+
target_revision = determine_target_revision(mirror, new_revision)
|
24
|
+
print " !!! UPDATE AVAILABLE !!!" if new_revision.to_s != mirror.base_revision.to_s
|
25
|
+
print "\n"
|
26
|
+
end
|
27
|
+
print "\n"
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,33 @@
|
|
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
|
+
commit_message = "Removed mirror '#{mirror.path}'"
|
27
|
+
git.commit(commit_message)
|
28
|
+
msg commit_message
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
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
|