gitenv 0.0.4 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,6 +1,115 @@
1
1
  # gitenv
2
2
 
3
- **Git environment project manager.**
3
+ Creates symlinks to your configuration files in a git repository (<a href="https://github.com/AlphaHydrae/env">like mine</a>).
4
+
5
+ Run gitenv without arguments to check the symlink configuration. First-time users will be prompted to enter the path to their environment repository so gitenv can set up its own config file.
6
+
7
+ #=> gitenv
8
+
9
+ ~/.gemrc -> ~/projects/env/.gemrc ok
10
+ ~/.gitconfig -> ~/projects/env/.gitconfig not yet set up
11
+ ~/.zshrc -> ~/projects/env/.zshrc not yet set up
12
+
13
+ Then run it with **update** to set up the missing symlinks.
14
+
15
+ #=> gitenv update
16
+
17
+ ~/.gemrc -> ~/projects/env/.gemrc ok
18
+ ~/.gitconfig -> ~/projects/env/.gitconfig ok
19
+ ~/.zshrc -> ~/projects/env/.zshrc ok
20
+
21
+ Read on for <a href="#configuration">more advanced options</a>.
22
+
23
+ ## Installation
24
+
25
+ gem install gitenv
26
+
27
+ <a name="configuration"></a>
28
+ ## Configuration
29
+
30
+ If your repository is more complex than a bunch of dot files or you want to put the links somewhere other than in your home folder, you will have to customize your configuration file. Gitenv prompts you to create one the first time you run it. It looks for `~/.gitenv.rb` by default. You can override this with the `-c, --config` option or by setting the `GITENV_CONFIG` environment variable.
31
+
32
+ The following sections demonstrate the various options you can customize in the configuration file.
33
+
34
+ ### Repository
35
+
36
+ ```ruby
37
+ # Path to your environment repository.
38
+ repo '~/projects/env'
39
+
40
+ # Note that you can also set the GITENV_REPO environment variable.
41
+ ```
42
+
43
+ ### Symlinks
44
+
45
+ ```ruby
46
+ # This will create symlinks in your home folder to all dot files (e.g. .vimrc, .gitconfig) in the repository.
47
+ symlink dot_files
48
+
49
+ # This creates symlinks for all files, not just dot files.
50
+ symlink all_files
51
+
52
+ # You can also create symlinks one at a time.
53
+ symlink '.zshrc'
54
+ symlink 'my_config_file'
55
+
56
+ # If you want the name of link to be different than that of the file, use the :as option.
57
+ symlink 'zshconfig', :as => '.zshconfig'
58
+ ```
59
+
60
+ ### Sub-folders in the repository
61
+
62
+ ```ruby
63
+ # If your configuration file or files are in a sub-folder, use #from to tell gitenv where to find them.
64
+ from 'subfolder' do
65
+ symlink dot_files
66
+ end
67
+
68
+ # The following syntax is also available.
69
+ symlink('.zshrc').from('zsh')
70
+ symlink(all_files).from('a/b/c')
71
+
72
+ # You can chain several #from calls to traverse hierarchies.
73
+ from 'a' do
74
+ from 'b' do
75
+ from 'c' do
76
+ # Symlink all dot files in a/b/c in your repository.
77
+ symlink dot_files
78
+ end
79
+ end
80
+ end
81
+ ```
82
+
83
+ ### Change the destination
84
+
85
+ ```ruby
86
+ # If you want to put the symlinks somewhere other than in your home folder, use #to.
87
+ to 'links' do
88
+ symlink dot_files
89
+ end
90
+
91
+ # The following syntax is also available.
92
+ symlink('.zshrc').to('links')
93
+ symlink(all_files).to('a/b/c')
94
+
95
+ # Paths are relative to your home folder. If you need an absolute path, use #to_abs.
96
+ symlink('my_config_file').to_abs('/opt/config')
97
+ ```
98
+
99
+ ### Copy files
100
+
101
+ ```ruby
102
+ # Gitenv can also copy files instead of creating symlinks.
103
+ copy dot_files
104
+
105
+ # Path modifiers work the same as for symlinks.
106
+ copy(dot_files).from('zsh')
107
+ copy('my_config_file').to('configs')
108
+ ```
109
+
110
+ ### Symlinks or copies in system directories
111
+
112
+ Just use `sudo`. Gitenv will happily set up your symlinks. You might have to add the `-E` switch to keep gitenv in the PATH.
4
113
 
5
114
  ## License (MIT)
6
115
 
data/Rakefile CHANGED
@@ -17,8 +17,8 @@ Jeweler::Tasks.new do |gem|
17
17
  gem.name = "gitenv"
18
18
  gem.homepage = "http://github.com/AlphaHydrae/gitenv"
19
19
  gem.license = "MIT"
20
- gem.summary = %Q{Git environment project manager.}
21
- gem.description = %Q{Creates symlinks to configuration files in a git repository.}
20
+ gem.summary = %Q{Symlink manager for git repositories with configuration files.}
21
+ gem.description = %Q{Gitenv sets up symlinks to your configuration files in a git repository.}
22
22
  gem.email = "hydrae.alpha@gmail.com"
23
23
  gem.authors = ["AlphaHydrae"]
24
24
  # dependencies defined in Gemfile
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.4
1
+ 0.0.5
data/gitenv.gemspec CHANGED
@@ -5,12 +5,12 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "gitenv"
8
- s.version = "0.0.4"
8
+ s.version = "0.0.5"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["AlphaHydrae"]
12
- s.date = "2012-09-23"
13
- s.description = "Creates symlinks to configuration files in a git repository."
12
+ s.date = "2012-09-24"
13
+ s.description = "Gitenv sets up symlinks to your configuration files in a git repository."
14
14
  s.email = "hydrae.alpha@gmail.com"
15
15
  s.executables = ["gitenv"]
16
16
  s.extra_rdoc_files = [
@@ -50,7 +50,7 @@ Gem::Specification.new do |s|
50
50
  s.licenses = ["MIT"]
51
51
  s.require_paths = ["lib"]
52
52
  s.rubygems_version = "1.8.24"
53
- s.summary = "Git environment project manager."
53
+ s.summary = "Symlink manager for git repositories with configuration files."
54
54
 
55
55
  if s.respond_to? :specification_version then
56
56
  s.specification_version = 3
data/lib/gitenv.rb CHANGED
@@ -2,7 +2,7 @@
2
2
  require 'paint'
3
3
 
4
4
  module Gitenv
5
- VERSION = '0.0.4'
5
+ VERSION = '0.0.5'
6
6
  end
7
7
 
8
8
  [ :context, :config, :controller, :symlink, :copy, :enumerator, :action ].each do |lib|
data/lib/gitenv/action.rb CHANGED
@@ -4,14 +4,20 @@ module Gitenv
4
4
  class Action
5
5
  include Context
6
6
 
7
- def initialize config, type, files
8
- @type, @files = type, files
7
+ def initialize config, type, files, options
8
+ @type, @files, @options = type, files, options
9
9
  copy! config
10
10
  end
11
11
 
12
- def each &block
13
- @files.each File.join(*[repository, from_path].compact) do |f|
14
- block.call @type.new(self, f)
12
+ def each options = {}, &block
13
+ @files.each from_path do |f|
14
+ block.call @type.new(self, f, @options.merge(options))
15
+ end
16
+ end
17
+
18
+ def each_file &block
19
+ @files.each from_path do |f|
20
+ block.call File.join(from_path, f)
15
21
  end
16
22
  end
17
23
  end
data/lib/gitenv/config.rb CHANGED
@@ -5,7 +5,7 @@ module Gitenv
5
5
  include Context
6
6
 
7
7
  attr_reader :actions
8
- attr_reader :repository
8
+ attr_accessor :repository
9
9
  attr_reader :home
10
10
 
11
11
  def initialize home
@@ -17,8 +17,12 @@ module Gitenv
17
17
  @repository = File.expand_path path
18
18
  end
19
19
 
20
- def symlink file
21
- Action.new(self, Symlink, enumerator(file)).tap{ |a| @actions << a }
20
+ def symlink file, options = {}
21
+ Action.new(self, Symlink, enumerator(file), options).tap{ |a| @actions << a }
22
+ end
23
+
24
+ def copy file, options = {}
25
+ Action.new(self, Copy, enumerator(file), options).tap{ |a| @actions << a }
22
26
  end
23
27
 
24
28
  def all_files
@@ -2,8 +2,7 @@
2
2
  module Gitenv
3
3
 
4
4
  module Context
5
- attr_accessor :home, :repository
6
- attr_accessor :from_paths, :to_paths
5
+ attr_accessor :from_paths, :to_paths, :absolute
7
6
 
8
7
  def from path, &block
9
8
  (@from_paths ||= []) << path
@@ -11,10 +10,11 @@ module Gitenv
11
10
  instance_eval &block
12
11
  @from_paths.pop
13
12
  end
13
+ self
14
14
  end
15
15
 
16
16
  def from_path
17
- @from_paths ? File.join(*@from_paths) : nil
17
+ @from_paths ? File.join(*([ @config.repository, @from_paths ].flatten)) : @config.repository
18
18
  end
19
19
 
20
20
  def to path, &block
@@ -23,17 +23,38 @@ module Gitenv
23
23
  instance_eval &block
24
24
  @to_paths.pop
25
25
  end
26
+ self
27
+ end
28
+
29
+ def to_abs path, &block
30
+ previous = @to_paths
31
+ @to_paths = [ path ]
32
+ @absolute = true
33
+ if block
34
+ instance_eval &block
35
+ @to_paths = previous
36
+ @absolute = false
37
+ end
38
+ self
26
39
  end
27
40
 
28
41
  def to_path
29
- @to_paths ? File.join(*@to_paths) : nil
42
+ @to_paths ? File.join(*(@absolute ? @to_paths : [ @config.home, @to_paths ]).flatten) : @config.home
43
+ end
44
+
45
+ def copy! config
46
+ self.from_paths = config.from_paths ? config.from_paths.dup : []
47
+ self.to_paths = config.to_paths ? config.to_paths.dup : []
48
+ self.absolute = config.absolute
49
+ @config = config
50
+ end
51
+
52
+ def home
53
+ @config.home
30
54
  end
31
55
 
32
- def copy! source
33
- self.from_paths = source.from_paths ? source.from_paths.dup : []
34
- self.to_paths = source.to_paths ? source.to_paths.dup : []
35
- self.repository = source.repository
36
- self.home = source.home
56
+ def repository
57
+ @config.repository
37
58
  end
38
59
  end
39
60
  end
@@ -1,3 +1,4 @@
1
+ require 'readline'
1
2
 
2
3
  module Gitenv
3
4
 
@@ -13,10 +14,30 @@ module Gitenv
13
14
  end
14
15
 
15
16
  def run
17
+
18
+ check_config_file!
19
+
20
+ if !File.exists?(config_file) and !repository
21
+ create_config_file!
22
+ end
23
+
16
24
  load_config_file!
25
+
26
+ @config.repository = repository
27
+ check_repository!
28
+
29
+ # load dot files by default
30
+ if @config.actions.empty?
31
+ @config.symlink @config.dot_files
32
+ end
33
+
34
+ check_files!
35
+
36
+ longest = 0
37
+ @config.actions.each{ |a| a.each{ |impl| longest = impl.description.length if impl.description.length > longest } }
38
+
17
39
  @config.actions.each do |a|
18
- a.each do |impl|
19
- impl.build!
40
+ a.each :justify => longest + 3 do |impl|
20
41
  impl.update! if @action == :update
21
42
  puts impl
22
43
  end
@@ -25,25 +46,123 @@ module Gitenv
25
46
 
26
47
  private
27
48
 
49
+ def create_config_file!
50
+
51
+ file = config_file
52
+ unless agree "You have no configuration file (#{file}); do you wish to create one? (y/n) "
53
+ puts
54
+ abort "To use gitenv, you must either create a configuration file\nor specify an environment repository with the -r, --repo option."
55
+ end
56
+
57
+ repo = @options.repo || ENV['GITENV_REPO']
58
+ unless repo
59
+ Readline.completion_append_character = '/'
60
+ Readline.completion_proc = Proc.new do |str|
61
+ Dir[str+'*'].grep /^#{Regexp.escape(str)}/
62
+ end
63
+ begin
64
+ repo = Readline.readline('Type the path to your environment repository: ', true)
65
+ rescue Interrupt
66
+ exit 1
67
+ end
68
+ end
69
+
70
+ if !repo or repo.strip.empty?
71
+ puts; abort "You must specify an environment repository."
72
+ elsif !File.directory?(File.expand_path(repo))
73
+ puts; abort "No such directory #{repo}."
74
+ end
75
+
76
+ config = String.new.tap do |s|
77
+ s << %|\n# Path to your environment repository.|
78
+ s << %|\nrepo "#{repo}"\n|
79
+ s << %|\n# Create symlinks in your home folder.|
80
+ s << %|\nsymlink dot_files\n\n|
81
+ end
82
+
83
+ File.open(file, 'w'){ |f| f.write config }
84
+
85
+ puts
86
+ puts Paint["Successfully wrote configuration to #{file}", :green]
87
+ puts
88
+ end
89
+
90
+ def repository
91
+ @options.repo || @config.repository || ENV['GITENV_REPO']
92
+ end
93
+
94
+ def config_file
95
+ if @options.config
96
+ File.expand_path @options.config
97
+ elsif ENV['GITENV_CONFIG']
98
+ File.expand_path ENV['GITENV_CONFIG']
99
+ else
100
+ File.join @home, '.gitenv.rb'
101
+ end
102
+ end
103
+
28
104
  def load_config_file!
29
-
30
- config_file = @options.config ? File.expand_path(@options.config) : File.join(@home, '.gitenv.rb')
31
- if !File.exists?(config_file) and @options.config
32
- abort "No such configuration file #{config_file}"
33
- elsif !File.exists?(config_file)
34
- abort "You must create a configuration file #{config_file} or load another one with the --config option"
35
- elsif !File.file?(config_file)
36
- abort "Configuration file #{config_file} is not a file"
37
- elsif !File.readable?(config_file)
38
- abort "Configuration file #{config_file} is not readable"
105
+ file = config_file
106
+ return unless File.exists? file
107
+ contents = File.open(file, 'r').read
108
+ @config.instance_eval contents, file
109
+ end
110
+
111
+ def check_files!
112
+ problems = []
113
+ @config.actions.each do |a|
114
+ a.each_file do |f|
115
+ if !File.exists?(f)
116
+ problems << { :file => f, :msg => "does not exist" }
117
+ elsif !File.file?(f)
118
+ problems << { :file => f, :msg => "is not a file" }
119
+ elsif !File.readable?(f)
120
+ problems << { :file => f, :msg => "is not readable" }
121
+ end
122
+ end
123
+ end
124
+
125
+ return unless problems.any?
126
+
127
+ msg = "There are problems with the following files in your repository:"
128
+ problems.each do |p|
129
+ msg << "\n #{p[:file]} #{p[:msg]}"
130
+ end
131
+ abort msg
132
+ end
133
+
134
+ def check_config_file!
135
+ file = config_file
136
+ return if !File.exists?(file)
137
+ if !File.file?(file)
138
+ abort "#{file} is not a file. It cannot be used as a configuration file."
139
+ elsif !File.readable?(file)
140
+ abort "#{file} is not readable. It cannot be used as a configuration file."
39
141
  end
142
+ end
40
143
 
41
- contents = File.open(config_file, 'r').read
42
- @config.instance_eval contents, config_file
144
+ def check_repository!
145
+ if !@config.repository
146
+ msg = "You have not specified an environment repository."
147
+ msg << "\nYou must either use the -r, --repo option or create"
148
+ msg << "\na configuration file (~/.gitenv.rb by default) with"
149
+ msg << "\nthe repo setting."
150
+ abort msg
151
+ end
152
+ return if File.directory? @config.repository
153
+ notice = File.exists?(@config.repository) ? 'is not a directory' : 'does not exist'
154
+ from = if @options.repo
155
+ "--repo #{@options.repo}"
156
+ elsif ENV['GITENV_REPO']
157
+ "$GITENV_REPO = #{ENV['GITENV_REPO']}"
158
+ else
159
+ %/repo "#{@config.repository}"/
160
+ end
161
+ abort "The repository you have specified #{notice}.\n (#{from})"
43
162
  end
44
163
 
45
164
  def abort msg, code = 1
46
- warn ms
165
+ warn Paint[msg, :red]
47
166
  exit code
48
167
  end
49
168
  end
data/lib/gitenv/copy.rb CHANGED
@@ -1,12 +1,68 @@
1
+ # encoding: UTF-8
2
+ require 'fileutils'
3
+ require 'digest/sha1'
1
4
 
2
5
  module Gitenv
3
6
 
4
7
  class Copy
5
- include Context
6
8
 
7
- def initialize config, file
8
- @config, @file = config, file
9
- copy! config
9
+ def initialize config, file, options = {}
10
+ @config, @file, @options = config, file, options
11
+ end
12
+
13
+ def update!
14
+ if File.exists?(target) && !File.exists?(target_copy)
15
+ FileUtils.mv target, target_copy
16
+ end
17
+ if !File.exists?(target)
18
+ FileUtils.copy source, target
19
+ end
20
+ end
21
+
22
+ def to_s
23
+ color, mark, msg = status
24
+ justification = @options[:justify] ? ' ' * (@options[:justify] - description.length) : ''
25
+ %/ #{Paint[mark, color]} #{Paint[target, :cyan]} << #{source}#{justification}#{Paint[msg, color]}/
26
+ end
27
+
28
+ def description
29
+ "#{target} << #{source}"
30
+ end
31
+
32
+ private
33
+
34
+ def status
35
+ if !File.exists?(target)
36
+ [ :blue, "✗", "is not set up; update will create the copy" ]
37
+ elsif digest(source) == digest(target)
38
+ [ :green, "✓", "ok" ]
39
+ elsif File.exists?(target_copy)
40
+ [ :red, "✗", "already exists with backup copy" ]
41
+ else
42
+ [ :blue, "✗", "already exists; update will backup the file and create the copy" ]
43
+ end
44
+ end
45
+
46
+ def target
47
+ @target ||= File.join(*[ @config.to_path, target_name].compact)
48
+ end
49
+
50
+ def target_copy
51
+ @target_copy ||= "#{target}.orig"
52
+ end
53
+
54
+ def target_name
55
+ @options[:as] || @file
56
+ end
57
+
58
+ def source
59
+ @source ||= File.join(*[ @config.from_path, @file ].compact)
60
+ end
61
+
62
+ def digest file
63
+ Digest::SHA1.new.tap do |dig|
64
+ File.open(file, 'rb'){ |io| dig.update io.readpartial(4096) while !io.eof }
65
+ end
10
66
  end
11
67
  end
12
68
  end
@@ -3,59 +3,58 @@
3
3
  module Gitenv
4
4
 
5
5
  class Symlink
6
- include Context
7
6
 
8
- def initialize config, file
9
- @config, @file = config, file
10
- copy! config
11
- end
12
-
13
- def build!
14
- @color, @mark, @message = if File.symlink? link
15
- @current_target = File.expand_path File.readlink(link)
16
- if @current_target == target
17
- [ :green, "✓", "ok" ]
18
- else
19
- [ :yellow, "✗", "currently points to #{@current_target}" ]
20
- end
21
- elsif File.file? link
22
- [ :red, "✗", "is a file" ]
23
- elsif File.directory? link
24
- [ :red, "✗", "is a directory" ]
25
- elsif File.exists? link
26
- [ :red, "✗", "exists but is not a symlink" ]
27
- else
28
- [ :blue, "✓", "is not set up" ]
29
- end
7
+ def initialize config, file, options = {}
8
+ @config, @file, @options = config, file, options
30
9
  end
31
10
 
32
11
  def update!
33
- if !File.exists? link
12
+ unless File.exists? link
34
13
  File.symlink target, link
35
- @color, @mark, @message = :green, "✓", "ok"
36
14
  end
37
15
  end
38
16
 
39
17
  def to_s
40
- %/ #{status_mark} #{Paint[link, :cyan]} -> #{target} #{status_message}/
18
+ color, mark, msg = status
19
+ justification = @options[:justify] ? ' ' * (@options[:justify] - description.length) : ''
20
+ %/ #{Paint[mark, color]} #{Paint[link, :cyan]} -> #{target}#{justification}#{Paint[msg, color]}/
41
21
  end
42
22
 
43
- private
44
-
45
- def status_mark
46
- Paint["#{@mark}", @color]
23
+ def description
24
+ "#{link} -> #{target}"
47
25
  end
48
26
 
49
- def status_message
50
- Paint["#{@message}", @color]
27
+ private
28
+
29
+ def status
30
+ if File.symlink? link
31
+ current_target = File.expand_path File.readlink(link)
32
+ if current_target == target
33
+ [ :green, "✓", "ok" ]
34
+ else
35
+ [ :yellow, "✗", "currently points to #{current_target}; update will overwrite" ]
36
+ end
37
+ elsif File.file? link
38
+ [ :red, "✗", "is a file; update will ignore" ]
39
+ elsif File.directory? link
40
+ [ :red, "✗", "is a directory; update will ignore" ]
41
+ elsif File.exists? link
42
+ [ :red, "✗", "exists but is not a symlink; update will ignore" ]
43
+ else
44
+ [ :blue, "✗", "is not set up; update will create the link" ]
45
+ end
51
46
  end
52
47
 
53
48
  def link
54
- File.join(*[ @config.home, to_path, @file].compact)
49
+ @link ||= File.join(*[ @config.to_path, link_name].compact)
55
50
  end
56
51
 
57
52
  def target
58
- File.join(*[ @config.repository, from_path, @file ].compact)
53
+ @target ||= File.join(*[ @config.from_path, @file ].compact)
54
+ end
55
+
56
+ def link_name
57
+ @options[:as] || @file
59
58
  end
60
59
  end
61
60
  end
data/lib/program.rb CHANGED
@@ -5,20 +5,27 @@ program :name, 'gitenv'
5
5
  program :version, Gitenv::VERSION
6
6
  program :description, 'Symlink manager for git repositories with configuration files.'
7
7
 
8
+ global_option '-r', '--repo PATH', 'Specify the path to the environment repository'
9
+ global_option '-c', '--config PATH', 'Use a custom configuration file (defaults to ~/.gitenv.rb)'
10
+
8
11
  command :info do |c|
12
+ c.syntax = 'gitenv info'
13
+ c.description = 'Display the current configuration (default action)'
9
14
  c.action do |args,options|
10
15
  Gitenv::Controller.new(:info, options).run
11
16
  end
12
17
  end
13
18
 
14
19
  command :update do |c|
20
+ c.syntax = 'gitenv update'
21
+ c.description = 'Create/update the symlinks'
15
22
  c.action do |args,options|
16
23
  Gitenv::Controller.new(:update, options).run
17
24
  end
18
25
  end
19
26
 
20
- global_option '-r', '--repo PATH', 'Specify the path to the environment repository'
21
- global_option '-c', '--config PATH', 'Use a custom configuration file (defaults to ~/.gitenv.rb)'
27
+ # TODO: add a verbose option (show repo, show config file path)
28
+ # TODO: add link to documentation in help
22
29
 
23
30
  default_command :info
24
31
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gitenv
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.0.5
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-09-23 00:00:00.000000000 Z
12
+ date: 2012-09-24 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: paint
@@ -171,7 +171,7 @@ dependencies:
171
171
  - - ! '>='
172
172
  - !ruby/object:Gem::Version
173
173
  version: '0'
174
- description: Creates symlinks to configuration files in a git repository.
174
+ description: Gitenv sets up symlinks to your configuration files in a git repository.
175
175
  email: hydrae.alpha@gmail.com
176
176
  executables:
177
177
  - gitenv
@@ -231,5 +231,5 @@ rubyforge_project:
231
231
  rubygems_version: 1.8.24
232
232
  signing_key:
233
233
  specification_version: 3
234
- summary: Git environment project manager.
234
+ summary: Symlink manager for git repositories with configuration files.
235
235
  test_files: []