braid 0.6.2 → 0.7.0
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 +3 -0
- data/README.textile +5 -10
- data/Rakefile +3 -0
- data/bin/braid +13 -5
- data/braid.gemspec +26 -18
- data/lib/braid.rb +17 -7
- data/lib/braid/command.rb +70 -69
- data/lib/braid/commands/add.rb +3 -3
- data/lib/braid/commands/list.rb +32 -0
- data/lib/braid/commands/push.rb +1 -1
- data/lib/braid/commands/setup.rb +19 -18
- data/lib/braid/commands/update.rb +70 -69
- data/lib/braid/config.rb +9 -9
- data/lib/braid/mirror.rb +49 -48
- data/lib/braid/operations.rb +98 -69
- data/lib/braid/version.rb +3 -0
- data/lib/core_ext.rb +1 -1
- data/test/config_test.rb +1 -1
- data/test/integration/adding_test.rb +2 -2
- data/test/integration/updating_test.rb +4 -4
- data/test/integration_helper.rb +3 -3
- data/test/mirror_test.rb +2 -2
- data/test/operations_test.rb +2 -2
- metadata +101 -95
data/lib/braid/commands/setup.rb
CHANGED
@@ -6,29 +6,30 @@ module Braid
|
|
6
6
|
end
|
7
7
|
|
8
8
|
protected
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
9
|
+
|
10
|
+
def setup_all
|
11
|
+
msg "Setting up all mirrors."
|
12
|
+
config.mirrors.each do |path|
|
13
|
+
setup_one(path)
|
14
14
|
end
|
15
|
+
end
|
15
16
|
|
16
|
-
|
17
|
-
|
17
|
+
def setup_one(path)
|
18
|
+
mirror = config.get!(path)
|
18
19
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
20
|
+
if git.remote_url(mirror.remote)
|
21
|
+
msg "Setup: Mirror '#{mirror.path}' already has a remote. Reusing it." if verbose?
|
22
|
+
return
|
23
|
+
end
|
23
24
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
end
|
25
|
+
msg "Setup: Creating remote for '#{mirror.path}'."
|
26
|
+
unless mirror.type == "svn"
|
27
|
+
url = use_local_cache? ? git_cache.path(mirror.url) : mirror.url
|
28
|
+
git.remote_add(mirror.remote, url, mirror.branch)
|
29
|
+
else
|
30
|
+
git_svn.init(mirror.remote, mirror.url)
|
31
31
|
end
|
32
|
+
end
|
32
33
|
end
|
33
34
|
end
|
34
35
|
end
|
@@ -10,92 +10,93 @@ module Braid
|
|
10
10
|
end
|
11
11
|
|
12
12
|
protected
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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
19
|
end
|
20
|
+
end
|
20
21
|
|
21
|
-
|
22
|
-
|
22
|
+
def update_one(path, options = {})
|
23
|
+
mirror = config.get!(path)
|
23
24
|
|
24
|
-
|
25
|
-
|
25
|
+
revision_message = options["revision"] ? " to #{display_revision(mirror, options["revision"])}" : ""
|
26
|
+
msg "Updating mirror '#{mirror.path}'#{revision_message}."
|
26
27
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
end
|
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
36
|
end
|
37
|
+
end
|
37
38
|
|
38
|
-
|
39
|
-
|
40
|
-
|
39
|
+
setup_remote(mirror)
|
40
|
+
msg "Fetching new commits for '#{mirror.path}'." if verbose?
|
41
|
+
mirror.fetch
|
41
42
|
|
42
|
-
|
43
|
-
|
43
|
+
new_revision = validate_new_revision(mirror, options["revision"])
|
44
|
+
target_revision = determine_target_revision(mirror, new_revision)
|
44
45
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
46
|
+
if mirror.merged?(target_revision)
|
47
|
+
msg "Mirror '#{mirror.path}' is already up to date."
|
48
|
+
return
|
49
|
+
end
|
49
50
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
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"]
|
54
58
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
local_hash = git.rev_parse("HEAD")
|
62
|
-
if !diff.empty?
|
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)
|
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)
|
71
65
|
else
|
72
|
-
|
66
|
+
base_hash = local_hash
|
73
67
|
end
|
74
|
-
|
75
|
-
|
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)
|
76
74
|
end
|
75
|
+
rescue Operations::MergeError => error
|
76
|
+
msg "Caught merge error. Breaking."
|
77
|
+
end
|
77
78
|
|
78
|
-
|
79
|
-
|
79
|
+
config.update(mirror)
|
80
|
+
add_config_file
|
80
81
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
end
|
86
|
-
|
87
|
-
git.commit(commit_message)
|
88
|
-
msg "Updated mirror to #{display_revision(mirror)}."
|
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
|
89
86
|
end
|
90
87
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
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
|
99
100
|
end
|
100
101
|
end
|
101
102
|
end
|
data/lib/braid/config.rb
CHANGED
@@ -79,10 +79,10 @@ module Braid
|
|
79
79
|
@db.roots.each do |path|
|
80
80
|
attributes = @db[path]
|
81
81
|
if attributes["local_branch"]
|
82
|
-
attributes["url"]
|
83
|
-
attributes["remote"]
|
82
|
+
attributes["url"] = attributes.delete("remote")
|
83
|
+
attributes["remote"] = attributes.delete("local_branch")
|
84
84
|
attributes["squashed"] = attributes.delete("squash")
|
85
|
-
attributes["lock"]
|
85
|
+
attributes["lock"] = attributes["revision"] # so far this has always been true
|
86
86
|
end
|
87
87
|
@db[path] = clean_attributes(attributes)
|
88
88
|
end
|
@@ -90,12 +90,12 @@ module Braid
|
|
90
90
|
end
|
91
91
|
|
92
92
|
private
|
93
|
-
|
94
|
-
|
95
|
-
|
93
|
+
def write_mirror(mirror)
|
94
|
+
@db[mirror.path] = clean_attributes(mirror.attributes)
|
95
|
+
end
|
96
96
|
|
97
|
-
|
98
|
-
|
99
|
-
|
97
|
+
def clean_attributes(hash)
|
98
|
+
hash.reject { |k, v| v.nil? }
|
99
|
+
end
|
100
100
|
end
|
101
101
|
end
|
data/lib/braid/mirror.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
module Braid
|
2
2
|
class Mirror
|
3
|
-
TYPES
|
3
|
+
TYPES = %w(git svn)
|
4
4
|
ATTRIBUTES = %w(url remote type branch squashed revision lock)
|
5
5
|
|
6
6
|
class UnknownType < BraidError
|
@@ -24,12 +24,12 @@ module Braid
|
|
24
24
|
attr_reader :path, :attributes
|
25
25
|
|
26
26
|
def initialize(path, attributes = {})
|
27
|
-
@path
|
27
|
+
@path = path.sub(/\/$/, '')
|
28
28
|
@attributes = attributes
|
29
29
|
end
|
30
30
|
|
31
31
|
def self.new_from_options(url, options = {})
|
32
|
-
url
|
32
|
+
url = url.sub(/\/$/, '')
|
33
33
|
|
34
34
|
branch = options["branch"] || "master"
|
35
35
|
|
@@ -47,11 +47,11 @@ module Braid
|
|
47
47
|
path = "vendor/plugins/#{path}"
|
48
48
|
end
|
49
49
|
|
50
|
-
remote
|
50
|
+
remote = "braid/#{path}".gsub("_", '-') # stupid git svn changes all _ to ., weird
|
51
51
|
squashed = !options["full"]
|
52
52
|
branch = nil if type == "svn"
|
53
53
|
|
54
|
-
attributes = {
|
54
|
+
attributes = {"url" => url, "remote" => remote, "type" => type, "branch" => branch, "squashed" => squashed}
|
55
55
|
self.new(path, attributes)
|
56
56
|
end
|
57
57
|
|
@@ -85,7 +85,7 @@ module Braid
|
|
85
85
|
|
86
86
|
def diff
|
87
87
|
remote_hash = git.rev_parse("#{base_revision}:")
|
88
|
-
local_hash
|
88
|
+
local_hash = git.tree_hash(path)
|
89
89
|
remote_hash != local_hash ? git.diff_tree(remote_hash, local_hash) : ""
|
90
90
|
end
|
91
91
|
|
@@ -119,59 +119,60 @@ module Braid
|
|
119
119
|
end
|
120
120
|
|
121
121
|
private
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
attributes[$1] = args[0]
|
128
|
-
end
|
122
|
+
|
123
|
+
def method_missing(name, *args)
|
124
|
+
if ATTRIBUTES.find { |attribute| name.to_s =~ /^(#{attribute})(=)?$/ }
|
125
|
+
unless $2
|
126
|
+
attributes[$1]
|
129
127
|
else
|
130
|
-
|
128
|
+
attributes[$1] = args[0]
|
131
129
|
end
|
130
|
+
else
|
131
|
+
raise NameError, "unknown attribute `#{name}'"
|
132
132
|
end
|
133
|
+
end
|
133
134
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
end
|
135
|
+
def inferred_revision
|
136
|
+
local_commits = git.rev_list("HEAD", "-- #{path}").split("\n")
|
137
|
+
remote_hashes = git.rev_list("--pretty=format:\"%T\"", remote).split("commit ").map do |chunk|
|
138
|
+
chunk.split("\n", 2).map { |value| value.strip }
|
139
|
+
end
|
140
|
+
hash = nil
|
141
|
+
local_commits.each do |local_commit|
|
142
|
+
local_tree = git.tree_hash(path, local_commit)
|
143
|
+
if match = remote_hashes.find { |_, remote_tree| local_tree == remote_tree }
|
144
|
+
hash = match[0]
|
145
|
+
break
|
146
146
|
end
|
147
|
-
hash
|
148
147
|
end
|
148
|
+
hash
|
149
|
+
end
|
149
150
|
|
150
|
-
|
151
|
-
|
152
|
-
|
151
|
+
def self.extract_type_from_url(url)
|
152
|
+
return nil unless url
|
153
|
+
url.sub!(/\/$/, '')
|
153
154
|
|
154
|
-
|
155
|
-
|
156
|
-
|
155
|
+
# check for git:// and svn:// URLs
|
156
|
+
url_scheme = url.split(":").first
|
157
|
+
return url_scheme if TYPES.include?(url_scheme)
|
157
158
|
|
158
|
-
|
159
|
-
|
160
|
-
|
159
|
+
return "svn" if url[-6..-1] == "/trunk"
|
160
|
+
return "git" if url[-4..-1] == ".git"
|
161
|
+
end
|
161
162
|
|
162
|
-
|
163
|
-
|
164
|
-
|
163
|
+
def self.extract_path_from_url(url)
|
164
|
+
return nil unless url
|
165
|
+
name = File.basename(url)
|
165
166
|
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
end
|
167
|
+
if File.extname(name) == ".git"
|
168
|
+
# strip .git
|
169
|
+
name[0..-5]
|
170
|
+
elsif name == "trunk"
|
171
|
+
# use parent
|
172
|
+
File.basename(File.dirname(url))
|
173
|
+
else
|
174
|
+
name
|
175
175
|
end
|
176
|
+
end
|
176
177
|
end
|
177
178
|
end
|
data/lib/braid/operations.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'singleton'
|
2
2
|
require 'rubygems'
|
3
|
-
require 'open4'
|
3
|
+
require defined?(JRUBY_VERSION) ? 'open3' : 'open4'
|
4
4
|
require 'tempfile'
|
5
5
|
|
6
6
|
module Braid
|
@@ -16,8 +16,8 @@ module Braid
|
|
16
16
|
end
|
17
17
|
class VersionTooLow < BraidError
|
18
18
|
def initialize(command, version, required)
|
19
|
-
@command
|
20
|
-
@version
|
19
|
+
@command = command
|
20
|
+
@version = version.to_s.split("\n").first
|
21
21
|
@required = required
|
22
22
|
end
|
23
23
|
|
@@ -45,8 +45,11 @@ module Braid
|
|
45
45
|
class Proxy
|
46
46
|
include Singleton
|
47
47
|
|
48
|
-
def self.command;
|
48
|
+
def self.command;
|
49
|
+
name.split('::').last.downcase;
|
50
|
+
end
|
49
51
|
|
52
|
+
# hax!
|
50
53
|
def version
|
51
54
|
status, out, err = exec!("#{self.class.command} --version")
|
52
55
|
out.sub(/^.* version/, "").strip
|
@@ -54,7 +57,7 @@ module Braid
|
|
54
57
|
|
55
58
|
def require_version(required)
|
56
59
|
required = required.split(".")
|
57
|
-
actual
|
60
|
+
actual = version.split(".")
|
58
61
|
|
59
62
|
actual.each_with_index do |actual_piece, idx|
|
60
63
|
required_piece = required[idx]
|
@@ -79,62 +82,72 @@ module Braid
|
|
79
82
|
end
|
80
83
|
|
81
84
|
private
|
82
|
-
def command(name)
|
83
|
-
# stub
|
84
|
-
name
|
85
|
-
end
|
86
85
|
|
87
|
-
|
88
|
-
|
89
|
-
|
86
|
+
def command(name)
|
87
|
+
# stub
|
88
|
+
name
|
89
|
+
end
|
90
90
|
|
91
|
-
|
92
|
-
|
93
|
-
|
91
|
+
def invoke(arg, *args)
|
92
|
+
exec!("#{command(arg)} #{args.join(' ')}".strip)[1].strip # return stdout
|
93
|
+
end
|
94
|
+
|
95
|
+
def method_missing(name, *args)
|
96
|
+
invoke(name, *args)
|
97
|
+
end
|
98
|
+
|
99
|
+
def exec(cmd)
|
100
|
+
cmd.strip!
|
94
101
|
|
95
|
-
|
96
|
-
|
102
|
+
previous_lang = ENV['LANG']
|
103
|
+
ENV['LANG'] = 'C'
|
97
104
|
|
98
|
-
|
99
|
-
|
105
|
+
out, err = nil
|
106
|
+
log(cmd)
|
100
107
|
|
101
|
-
|
102
|
-
|
108
|
+
if defined?(JRUBY_VERSION)
|
109
|
+
Open3.popen3(cmd) do |stdin, stdout, stderr|
|
110
|
+
out = stdout.read
|
111
|
+
err = stderr.read
|
112
|
+
end
|
113
|
+
status = $?.exitstatus
|
114
|
+
else
|
103
115
|
status = Open4.popen4(cmd) do |pid, stdin, stdout, stderr|
|
104
116
|
out = stdout.read
|
105
117
|
err = stderr.read
|
106
118
|
end.exitstatus
|
107
|
-
[status, out, err]
|
108
|
-
|
109
|
-
ensure
|
110
|
-
ENV['LANG'] = previous_lang
|
111
119
|
end
|
112
120
|
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
end
|
121
|
+
[status, out, err]
|
122
|
+
ensure
|
123
|
+
ENV['LANG'] = previous_lang
|
124
|
+
end
|
118
125
|
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
true
|
125
|
-
end
|
126
|
+
def exec!(cmd)
|
127
|
+
status, out, err = exec(cmd)
|
128
|
+
raise ShellExecutionError, err unless status == 0
|
129
|
+
[status, out, err]
|
130
|
+
end
|
126
131
|
|
127
|
-
|
128
|
-
|
129
|
-
|
132
|
+
def sh(cmd, message = nil)
|
133
|
+
message ||= "could not fetch" if cmd =~ /fetch/
|
134
|
+
log(cmd)
|
135
|
+
`#{cmd}`
|
136
|
+
raise ShellExecutionError, message unless $?.exitstatus == 0
|
137
|
+
true
|
138
|
+
end
|
130
139
|
|
131
|
-
|
132
|
-
|
133
|
-
|
140
|
+
def msg(str)
|
141
|
+
puts "Braid: #{str}"
|
142
|
+
end
|
134
143
|
|
135
|
-
|
136
|
-
|
137
|
-
|
144
|
+
def log(cmd)
|
145
|
+
msg "Executing `#{cmd}`" if verbose?
|
146
|
+
end
|
147
|
+
|
148
|
+
def verbose?
|
149
|
+
Braid.verbose
|
150
|
+
end
|
138
151
|
end
|
139
152
|
|
140
153
|
class Git < Proxy
|
@@ -275,12 +288,22 @@ module Braid
|
|
275
288
|
|
276
289
|
def apply(diff, *args)
|
277
290
|
err = nil
|
278
|
-
status = Open4.popen4("git apply --index --whitespace=nowarn #{args.join(' ')} -") do |pid, stdin, stdout, stderr|
|
279
|
-
stdin.puts(diff)
|
280
|
-
stdin.close
|
281
291
|
|
282
|
-
|
283
|
-
|
292
|
+
if defined?(JRUBY_VERSION)
|
293
|
+
Open3.popen3("git apply --index --whitespace=nowarn #{args.join(' ')} -") do |stdin, stdout, stderr|
|
294
|
+
stdin.puts(diff)
|
295
|
+
stdin.close
|
296
|
+
err = stderr.read
|
297
|
+
end
|
298
|
+
status = $?.exitstatus
|
299
|
+
else
|
300
|
+
status = Open4.popen4("git apply --index --whitespace=nowarn #{args.join(' ')} -") do |pid, stdin, stdout, stderr|
|
301
|
+
stdin.puts(diff)
|
302
|
+
stdin.close
|
303
|
+
err = stderr.read
|
304
|
+
end.exitstatus
|
305
|
+
end
|
306
|
+
|
284
307
|
raise ShellExecutionError, err unless status == 0
|
285
308
|
true
|
286
309
|
end
|
@@ -291,17 +314,21 @@ module Braid
|
|
291
314
|
end
|
292
315
|
|
293
316
|
private
|
294
|
-
|
295
|
-
|
296
|
-
|
317
|
+
|
318
|
+
def command(name)
|
319
|
+
"#{self.class.command} #{name.to_s.gsub('_', '-')}"
|
320
|
+
end
|
297
321
|
end
|
298
322
|
|
299
323
|
class GitSvn < Proxy
|
300
|
-
def self.command;
|
324
|
+
def self.command;
|
325
|
+
"git svn";
|
326
|
+
end
|
301
327
|
|
302
328
|
def commit_hash(remote, revision)
|
303
|
-
out
|
304
|
-
part = out.to_s.split("
|
329
|
+
out = invoke(:log, "--show-commit --oneline", "-r #{revision}", remote)
|
330
|
+
part = out.to_s.split("|")[1]
|
331
|
+
part.strip!
|
305
332
|
raise UnknownRevision, "r#{revision}" unless part
|
306
333
|
git.rev_parse(part)
|
307
334
|
end
|
@@ -316,13 +343,14 @@ module Braid
|
|
316
343
|
end
|
317
344
|
|
318
345
|
private
|
319
|
-
def command(name)
|
320
|
-
"#{self.class.command} #{name}"
|
321
|
-
end
|
322
346
|
|
323
|
-
|
324
|
-
|
325
|
-
|
347
|
+
def command(name)
|
348
|
+
"#{self.class.command} #{name}"
|
349
|
+
end
|
350
|
+
|
351
|
+
def git
|
352
|
+
Git.instance
|
353
|
+
end
|
326
354
|
end
|
327
355
|
|
328
356
|
class Svn < Proxy
|
@@ -364,13 +392,14 @@ module Braid
|
|
364
392
|
end
|
365
393
|
|
366
394
|
private
|
367
|
-
def local_cache_dir
|
368
|
-
Braid.local_cache_dir
|
369
|
-
end
|
370
395
|
|
371
|
-
|
372
|
-
|
373
|
-
|
396
|
+
def local_cache_dir
|
397
|
+
Braid.local_cache_dir
|
398
|
+
end
|
399
|
+
|
400
|
+
def git
|
401
|
+
Git.instance
|
402
|
+
end
|
374
403
|
end
|
375
404
|
|
376
405
|
module VersionControl
|