avodeploy 0.4 → 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +14 -0
- data/README.md +8 -5
- data/avodeploy.gemspec +3 -4
- data/bin/avo +14 -12
- data/lib/avodeploy.rb +29 -0
- data/lib/{avocado → avodeploy}/bootstrap.rb +24 -30
- data/lib/{avocado → avodeploy}/command_execution_result.rb +1 -1
- data/lib/{avocado → avodeploy}/config.rb +8 -4
- data/lib/{avocado → avodeploy}/core_ext/hash_insert_at.rb +0 -0
- data/lib/{avocado → avodeploy}/core_ext/string_colors.rb +0 -0
- data/lib/{avocado → avodeploy}/deployment.rb +3 -3
- data/lib/{avocado → avodeploy}/multi_io.rb +1 -1
- data/lib/avodeploy/scm_provider/git_scm_provider.rb +73 -0
- data/lib/avodeploy/scm_provider/scm_provider.rb +70 -0
- data/lib/{avocado → avodeploy}/skel/manifest_template.rb.erb +1 -1
- data/lib/{avocado → avodeploy}/strategy/base.rb +1 -1
- data/lib/avodeploy/strategy/local_copy.rb +101 -0
- data/lib/{avocado → avodeploy}/target.rb +3 -1
- data/lib/avodeploy/task/local_task_execution_environment.rb +127 -0
- data/lib/avodeploy/task/remote_task_execution_environment.rb +114 -0
- data/lib/avodeploy/task/task.rb +76 -0
- data/lib/{avocado → avodeploy}/task/task_dependency.rb +8 -6
- data/lib/avodeploy/task/task_execution_environment.rb +88 -0
- data/lib/avodeploy/task/task_manager.rb +185 -0
- data/lib/{avocado → avodeploy}/version.rb +2 -2
- metadata +27 -40
- data/lib/avocado/Gemfile +0 -9
- data/lib/avocado/scm_provider/git_scm_provider.rb +0 -71
- data/lib/avocado/scm_provider/scm_provider.rb +0 -68
- data/lib/avocado/strategy/local_copy.rb +0 -99
- data/lib/avocado/task/local_task_execution_environment.rb +0 -127
- data/lib/avocado/task/remote_task_execution_environment.rb +0 -112
- data/lib/avocado/task/task.rb +0 -84
- data/lib/avocado/task/task_execution_environment.rb +0 -86
- data/lib/avocado/task/task_manager.rb +0 -183
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 35c82df62a686a9bb6e446bb38a53f862e52e563
|
4
|
+
data.tar.gz: 86b4cd123a894fa73837bc191d7f97635ccebc68
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0c919f8c31d1787f4c77ecd9a01afda0d0cd8e8f139874d62cd3fa6af6d2ffb842e619cacbee5b1fed06877a0ddbe975dda333257ac7eb3a9d0ada2773b58464
|
7
|
+
data.tar.gz: 84bacc8363fdb08d6acb6a9477923315cdcce8d994dfbf91e09a8d0a3542afa01c9b4d34e652ea6a8ddb71215baace5ea9fd473270a28a2fe93e63a5ec02c232
|
data/CHANGELOG
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
0.4.1
|
2
|
+
* local_copy strategy now checks, if one or more target systems are specified
|
3
|
+
* avo now raises an error if no valid stage is selected (avo [stage] deploy does not complain if the stage does not exist #7)
|
4
|
+
* Added `default` stage (useful in some cases)
|
5
|
+
* `avo ... -d` now raises exceptions directly
|
6
|
+
* Renamed Avocado's root module to `AvoDeploy` for gem naming convention compatibility
|
7
|
+
* Avocado's APIs can now be used by requiring 'avodeploy'
|
8
|
+
* Removed Avocado::Task::Task.pretty_print
|
9
|
+
* Tasks and ScmProviders are now organized in submodules
|
10
|
+
* terminal-table gem now fix at version 1.4.5
|
11
|
+
* Removed awesome_print dependency
|
12
|
+
* Remote code execution: target name will now be displayed
|
13
|
+
* local_copy strategy: `Avofile` is now excluded by default
|
14
|
+
* Tasks: Default directory is now the working copy (Task usability - chdir #5)
|
data/README.md
CHANGED
@@ -1,16 +1,13 @@
|
|
1
1
|
### Avocado
|
2
2
|
Avocado is a deployment framework for web applications written in Ruby.
|
3
3
|
|
4
|
-
The current release is 0.4.
|
4
|
+
The current release is 0.4.1.
|
5
5
|
|
6
6
|
### Licensing
|
7
7
|
Avocado is licensed under the GPLv2. For more Information have a look into the LICENSE file.
|
8
8
|
|
9
|
-
### Limitations
|
10
|
-
Avocado does currently only work if you use Git for as version control system and SSH to deploy. Furthermore, the only implemented authentication method for both SSH and Git is public key authentication.
|
11
|
-
|
12
9
|
### Getting started
|
13
|
-
Be sure that you have installed Ruby 2.
|
10
|
+
Be sure that you have installed Ruby 2.0 or higher and RubyGems. Then the Installation is as easy as
|
14
11
|
|
15
12
|
```
|
16
13
|
$ gem install avodeploy
|
@@ -27,6 +24,12 @@ $ avo install
|
|
27
24
|
|
28
25
|
to let Avocado place a deployment manifest file, called `Avofile`, in your project root folder. Then just open up the file in your editor of choice to customize your deployment process. The file is fully commented, giving you a basic understanding of how Avocado helps you to get your deployments done.
|
29
26
|
|
27
|
+
### Limitations
|
28
|
+
Avocado does currently only work if you use Git for as version control system and SSH to deploy. Furthermore, the only implemented authentication method for both SSH and Git is public key authentication.
|
29
|
+
|
30
|
+
### Changelog
|
31
|
+
There is a [CHANGELOG](CHANGELOG) file that contains all the changes throughout every version.
|
32
|
+
|
30
33
|
### Need more documentation?
|
31
34
|
I'm currently creating documents that both the end user and developers might find helpful.
|
32
35
|
|
data/avodeploy.gemspec
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
# coding: utf-8
|
2
2
|
lib = File.expand_path('../lib', __FILE__)
|
3
3
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
-
require '
|
4
|
+
require 'avodeploy/version'
|
5
5
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
7
|
spec.name = "avodeploy"
|
8
|
-
spec.version =
|
8
|
+
spec.version = AvoDeploy::VERSION
|
9
9
|
spec.authors = ["David Prandzioch"]
|
10
10
|
spec.email = ["dprandzioch@me.com"]
|
11
11
|
spec.summary = %q{Avocado is a flexible deployment framework for web applications.}
|
@@ -21,8 +21,7 @@ Gem::Specification.new do |spec|
|
|
21
21
|
spec.add_development_dependency "bundler", "~> 1.7"
|
22
22
|
spec.add_development_dependency "rake", "~> 10.0"
|
23
23
|
|
24
|
-
spec.add_dependency "
|
25
|
-
spec.add_dependency "terminal-table"
|
24
|
+
spec.add_dependency "terminal-table", "~> 1.4.5"
|
26
25
|
spec.add_dependency "net-ssh", "~> 2.9.1"
|
27
26
|
spec.add_dependency "net-scp", "~> 1.2.1"
|
28
27
|
spec.add_dependency "thor", "~> 0.19.1"
|
data/bin/avo
CHANGED
@@ -18,15 +18,10 @@
|
|
18
18
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
19
19
|
=end
|
20
20
|
|
21
|
-
require 'awesome_print'
|
22
|
-
require 'terminal-table'
|
23
|
-
require 'open3'
|
24
|
-
require 'net/ssh'
|
25
|
-
require 'net/scp'
|
26
21
|
require 'thor'
|
27
22
|
|
28
23
|
THIS_FILE = File.symlink?(__FILE__) ? File.readlink(__FILE__) : __FILE__
|
29
|
-
require File.join(File.dirname(THIS_FILE), '../lib/
|
24
|
+
require File.join(File.dirname(THIS_FILE), '../lib/avodeploy.rb')
|
30
25
|
|
31
26
|
class AvocadoCli < Thor
|
32
27
|
|
@@ -45,7 +40,7 @@ class AvocadoCli < Thor
|
|
45
40
|
right task chain for your needs.
|
46
41
|
LONGDESC
|
47
42
|
def start(stage, task)
|
48
|
-
instance =
|
43
|
+
instance = AvoDeploy::Bootstrap.run(stage, options[:verbose], options[:debug])
|
49
44
|
|
50
45
|
if options[:nodeps]
|
51
46
|
instance.task_manager.invoke_task_oneshot(task)
|
@@ -64,14 +59,21 @@ class AvocadoCli < Thor
|
|
64
59
|
stage = :default
|
65
60
|
end
|
66
61
|
|
67
|
-
instance =
|
68
|
-
instance.task_manager.task_by_name(name)
|
62
|
+
instance = AvoDeploy::Bootstrap.run(stage, options[:verbose], options[:debug])
|
63
|
+
task = instance.task_manager.task_by_name(name)
|
64
|
+
|
65
|
+
puts Terminal::Table.new :title => 'Task overview', :rows => [
|
66
|
+
['Name', task.name],
|
67
|
+
['Description', task.desc],
|
68
|
+
['Scope', task.scope],
|
69
|
+
['Visibility', task.visibility],
|
70
|
+
]
|
69
71
|
end
|
70
72
|
|
71
73
|
desc "chains [STAGE]", "List all chains for the current stage"
|
72
74
|
method_options :stage => :string
|
73
75
|
def chains(stage)
|
74
|
-
instance =
|
76
|
+
instance = AvoDeploy::Bootstrap.run(stage, options[:verbose], options[:debug])
|
75
77
|
|
76
78
|
instance.task_manager.chains.each do |chain|
|
77
79
|
rows = []
|
@@ -94,7 +96,7 @@ class AvocadoCli < Thor
|
|
94
96
|
|
95
97
|
desc "stages", "List all stages"
|
96
98
|
def stages
|
97
|
-
instance =
|
99
|
+
instance = AvoDeploy::Bootstrap.run(:default, options[:verbose], options[:debug])
|
98
100
|
|
99
101
|
puts Terminal::Table.new :title => "Stages list",
|
100
102
|
:headings => ['Title', 'Description'],
|
@@ -120,7 +122,7 @@ class AvocadoCli < Thor
|
|
120
122
|
}
|
121
123
|
|
122
124
|
require 'erb'
|
123
|
-
renderer = ERB.new(File.new(File.dirname(THIS_FILE) + '/../lib/
|
125
|
+
renderer = ERB.new(File.new(File.dirname(THIS_FILE) + '/../lib/avodeploy/skel/manifest_template.rb.erb').read)
|
124
126
|
output = renderer.result(binding)
|
125
127
|
|
126
128
|
File.write(Dir.pwd.concat('/Avofile'), output)
|
data/lib/avodeploy.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'terminal-table'
|
2
|
+
require 'open3'
|
3
|
+
require 'net/ssh'
|
4
|
+
require 'net/scp'
|
5
|
+
|
6
|
+
require 'avodeploy/core_ext/string_colors.rb'
|
7
|
+
require 'avodeploy/core_ext/hash_insert_at.rb'
|
8
|
+
|
9
|
+
require 'avodeploy/task/task.rb'
|
10
|
+
require 'avodeploy/task/task_dependency.rb'
|
11
|
+
require 'avodeploy/task/task_manager.rb'
|
12
|
+
require 'avodeploy/task/task_execution_environment.rb'
|
13
|
+
require 'avodeploy/task/local_task_execution_environment.rb'
|
14
|
+
require 'avodeploy/task/remote_task_execution_environment.rb'
|
15
|
+
|
16
|
+
require 'avodeploy/scm_provider/scm_provider.rb'
|
17
|
+
require 'avodeploy/scm_provider/git_scm_provider.rb'
|
18
|
+
|
19
|
+
require 'avodeploy/multi_io.rb'
|
20
|
+
require 'avodeploy/command_execution_result.rb'
|
21
|
+
require 'avodeploy/target.rb'
|
22
|
+
require 'avodeploy/config.rb'
|
23
|
+
require 'avodeploy/deployment.rb'
|
24
|
+
require 'avodeploy/bootstrap.rb'
|
25
|
+
|
26
|
+
module AvoDeploy
|
27
|
+
LIBNAME = 'avodeploy'
|
28
|
+
LIBDIR = File.expand_path("../#{LIBNAME}", __FILE__)
|
29
|
+
end
|
@@ -16,48 +16,34 @@
|
|
16
16
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
17
17
|
=end
|
18
18
|
|
19
|
-
module
|
19
|
+
module AvoDeploy
|
20
20
|
class Bootstrap
|
21
21
|
# Runs the avocado bootstrap
|
22
22
|
#
|
23
23
|
# @param stage [Symbol] the stage to bootstrap
|
24
24
|
# @param verbose [Boolean] run in verbose mode
|
25
25
|
# @param debug [Boolean] run in boolean mode
|
26
|
-
def self.run(stage, verbose = false, debug = false)
|
26
|
+
def self.run(stage = :default, verbose = false, debug = false)
|
27
27
|
if stage.is_a?(String)
|
28
28
|
stage = stage.to_sym
|
29
29
|
end
|
30
30
|
|
31
|
-
require File.join(File.dirname(__FILE__), 'core_ext/string_colors.rb')
|
32
|
-
require File.join(File.dirname(__FILE__), 'core_ext/hash_insert_at.rb')
|
33
|
-
|
34
|
-
require File.join(File.dirname(__FILE__), 'task/task.rb')
|
35
|
-
require File.join(File.dirname(__FILE__), 'task/task_dependency.rb')
|
36
|
-
require File.join(File.dirname(__FILE__), 'task/task_manager.rb')
|
37
|
-
require File.join(File.dirname(__FILE__), 'task/task_execution_environment.rb')
|
38
|
-
require File.join(File.dirname(__FILE__), 'task/local_task_execution_environment.rb')
|
39
|
-
require File.join(File.dirname(__FILE__), 'task/remote_task_execution_environment.rb')
|
40
|
-
|
41
|
-
require File.join(File.dirname(__FILE__), 'scm_provider/scm_provider.rb')
|
42
|
-
require File.join(File.dirname(__FILE__), 'scm_provider/git_scm_provider.rb')
|
43
|
-
|
44
|
-
require File.join(File.dirname(__FILE__), 'multi_io.rb')
|
45
|
-
require File.join(File.dirname(__FILE__), 'command_execution_result.rb')
|
46
|
-
require File.join(File.dirname(__FILE__), 'target.rb')
|
47
|
-
require File.join(File.dirname(__FILE__), 'config.rb')
|
48
|
-
require File.join(File.dirname(__FILE__), 'deployment.rb')
|
49
|
-
|
50
31
|
begin
|
51
32
|
# defaults
|
52
|
-
|
33
|
+
AvoDeploy::Deployment.configure do
|
53
34
|
set :stage, stage
|
35
|
+
|
36
|
+
setup_stage :default do
|
37
|
+
# a default stage is needed for some use cases,
|
38
|
+
# especially if you don't know which stages were defined by the user
|
39
|
+
end
|
54
40
|
end
|
55
41
|
|
56
42
|
if File.exist?(Dir.pwd.concat('/Avofile')) == false
|
57
43
|
raise RuntimeError, 'Could not find Avofile. Run `avo install` first.'
|
58
44
|
end
|
59
45
|
|
60
|
-
instance =
|
46
|
+
instance = AvoDeploy::Deployment.instance
|
61
47
|
|
62
48
|
# load user config initially to determine strategy
|
63
49
|
begin
|
@@ -67,6 +53,11 @@ module Avocado
|
|
67
53
|
# error not neccessary because dependencies are not loaded
|
68
54
|
end
|
69
55
|
|
56
|
+
# requested stage was not found
|
57
|
+
if instance.config.loaded_stage.nil?
|
58
|
+
raise ArgumentError, 'The requested stage does not exist.'
|
59
|
+
end
|
60
|
+
|
70
61
|
if debug
|
71
62
|
instance.log.level = Logger::DEBUG
|
72
63
|
elsif verbose
|
@@ -78,22 +69,25 @@ module Avocado
|
|
78
69
|
instance.log.debug "Loading deployment strategy #{instance.config.get(:strategy).to_s}..."
|
79
70
|
|
80
71
|
# load strategy
|
81
|
-
# @todo
|
82
|
-
require
|
83
|
-
require
|
84
|
-
|
72
|
+
# @todo check
|
73
|
+
require "avodeploy/strategy/base.rb"
|
74
|
+
require "avodeploy/strategy/#{instance.config.get(:strategy).to_s}.rb"
|
85
75
|
|
86
76
|
instance.log.debug "Loading user configuration..."
|
87
77
|
|
88
78
|
# override again by user config to allow manipulation of tasks
|
89
79
|
load File.join(Dir.pwd, 'Avofile')
|
90
80
|
rescue Exception => e
|
91
|
-
|
92
|
-
|
81
|
+
if debug
|
82
|
+
raise e
|
83
|
+
else
|
84
|
+
AvoDeploy::Deployment.instance.log.error e.message.red
|
85
|
+
end
|
86
|
+
|
93
87
|
Kernel.exit(true)
|
94
88
|
end
|
95
89
|
|
96
|
-
|
90
|
+
AvoDeploy::Deployment.instance
|
97
91
|
end
|
98
92
|
end
|
99
93
|
end
|
@@ -16,18 +16,20 @@
|
|
16
16
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
17
17
|
=end
|
18
18
|
|
19
|
-
module
|
19
|
+
module AvoDeploy
|
20
20
|
class Config
|
21
21
|
|
22
22
|
attr_reader :config
|
23
23
|
attr_reader :stages
|
24
24
|
attr_reader :targets
|
25
|
+
attr_reader :loaded_stage
|
25
26
|
|
26
27
|
# Intializes the config object
|
27
28
|
def initialize
|
28
29
|
@config = setup_config_defaults
|
29
30
|
@stages = {}
|
30
31
|
@targets = {}
|
32
|
+
@loaded_stage = nil
|
31
33
|
end
|
32
34
|
|
33
35
|
# Sets a configuration item
|
@@ -54,7 +56,7 @@ module Avocado
|
|
54
56
|
# @param options [Hash] task options
|
55
57
|
# @param block [Block] the code to be executed when the task is started
|
56
58
|
def task(name, options = {}, &block)
|
57
|
-
|
59
|
+
AvoDeploy::Deployment.instance.task_manager.add_task(name, options, &block)
|
58
60
|
end
|
59
61
|
|
60
62
|
# Defines a stage
|
@@ -64,11 +66,13 @@ module Avocado
|
|
64
66
|
# @param block [Block] the stage configuration
|
65
67
|
def setup_stage(name, options = {}, &block)
|
66
68
|
if options.has_key?(:desc)
|
67
|
-
stages[name] =
|
69
|
+
stages[name] = options[:desc]
|
68
70
|
end
|
69
71
|
|
70
72
|
if name == get(:stage)
|
71
73
|
instance_eval(&block)
|
74
|
+
|
75
|
+
@loaded_stage = name
|
72
76
|
end
|
73
77
|
end
|
74
78
|
|
@@ -77,7 +81,7 @@ module Avocado
|
|
77
81
|
# @param name [Symbol] the deployment targets' name
|
78
82
|
# @param options [Hash] target options
|
79
83
|
def target(name, options = {})
|
80
|
-
@targets[name] =
|
84
|
+
@targets[name] = AvoDeploy::Target.new(name, options)
|
81
85
|
end
|
82
86
|
|
83
87
|
# Merges the configuration with another config hash
|
File without changes
|
File without changes
|
@@ -16,7 +16,7 @@
|
|
16
16
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
17
17
|
=end
|
18
18
|
|
19
|
-
module
|
19
|
+
module AvoDeploy
|
20
20
|
class Deployment
|
21
21
|
|
22
22
|
attr_accessor :config
|
@@ -27,8 +27,8 @@ module Avocado
|
|
27
27
|
# Initializes the deployment
|
28
28
|
def initialize
|
29
29
|
@stages = {}
|
30
|
-
@task_manager =
|
31
|
-
@config =
|
30
|
+
@task_manager = AvoDeploy::Task::TaskManager.new
|
31
|
+
@config = AvoDeploy::Config.new
|
32
32
|
|
33
33
|
@log = ::Logger.new(STDOUT)
|
34
34
|
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
=begin
|
2
|
+
AVOCADO
|
3
|
+
The flexible and easy to use deployment framework for web applications
|
4
|
+
|
5
|
+
This program is free software; you can redistribute it and/or
|
6
|
+
modify it under the terms of the GNU General Public License Version 2
|
7
|
+
as published by the Free Software Foundation.
|
8
|
+
|
9
|
+
This program is distributed in the hope that it will be useful,
|
10
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
11
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
12
|
+
GNU General Public License for more details.
|
13
|
+
|
14
|
+
You should have received a copy of the GNU General Public License
|
15
|
+
along with this program; if not, write to the Free Software
|
16
|
+
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
17
|
+
=end
|
18
|
+
|
19
|
+
module AvoDeploy
|
20
|
+
module ScmProvider
|
21
|
+
class GitScmProvider
|
22
|
+
|
23
|
+
# Initializes the provider
|
24
|
+
#
|
25
|
+
# @param env [TaskExecutionEnvironment] Environment for the commands to be executed in
|
26
|
+
def initialize(env)
|
27
|
+
raise ArgumentError, "env must be a TaskExecutionEnvironment" unless env.kind_of?(AvoDeploy::Task::TaskExecutionEnvironment)
|
28
|
+
|
29
|
+
@env = env
|
30
|
+
end
|
31
|
+
|
32
|
+
# Checks out repository code from a system and switches to the given branch
|
33
|
+
#
|
34
|
+
# @param url [String] the repository location
|
35
|
+
# @param local_dir [String] path to the working copy
|
36
|
+
# @param branch [String] the branch to check out
|
37
|
+
def checkout_from_remote(url, local_dir, branch)
|
38
|
+
res = @env.command("git clone #{url} #{local_dir}")
|
39
|
+
raise RuntimeError, "Could not clone from git url #{url}" unless res.retval == 0
|
40
|
+
|
41
|
+
@env.chdir(local_dir)
|
42
|
+
res = @env.command("git checkout #{branch}")
|
43
|
+
@env.chdir('../')
|
44
|
+
|
45
|
+
raise RuntimeError, "could not switch to branch #{branch}" unless res.retval == 0
|
46
|
+
end
|
47
|
+
|
48
|
+
# Returns the current revision of the working copy
|
49
|
+
#
|
50
|
+
# @return [String] the current revision of the working copy
|
51
|
+
def revision
|
52
|
+
res = @env.command("git rev-parse HEAD")
|
53
|
+
|
54
|
+
res.stdout.gsub("\n", "")
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns scm files to be executed in the deployment process
|
58
|
+
#
|
59
|
+
# @return [Array] array of scm control files
|
60
|
+
def scm_files
|
61
|
+
[ '.git', '.gitignore' ]
|
62
|
+
end
|
63
|
+
|
64
|
+
# Returns the scm tools that have to be installed on specific systems
|
65
|
+
#
|
66
|
+
# @return [Array] array of utilities
|
67
|
+
def cli_utils
|
68
|
+
[ 'git' ]
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|