lace 0.5.1 → 0.5.3

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 46881767c22f451c4e42104713158a054acfe41eee483ab2cac43416d8e99738
4
- data.tar.gz: b8d483c8695e034ddd022522362b8aaad46a9947c98bf02bd92f9880cbda3280
3
+ metadata.gz: 0e9374d4a207b650f55ec19e285891ac00fc55da706d5a87b8ce5ff642315f30
4
+ data.tar.gz: 457594f9250c5e7045a00b227596c41f461a8487e3be6c95240f6891b8067e6e
5
5
  SHA512:
6
- metadata.gz: 3200530a5a2007710f79508f5027c1caa72bf08377f296a6adb6b94db404a7b6dca9abbcd5dfe7d9e8bc1945741c9f61299e06cbf7e15a5ac74e2ced4d81ee1e
7
- data.tar.gz: 0a6019e2cae178f0a276ecc943466445f493b6a30df7f0ae5f36db9a344b2641bfde336cced8e6be47e02b979ed7d749632d6e0057e544a65d21b50f3c219242
6
+ metadata.gz: f546f2835196c35b71522b3683376c79c3deee6b82529c24108639d44fa148bca4a1127aefcab3d6e38209b8c7cda917a535019fb1f2068db92d06427817b8b2
7
+ data.tar.gz: 386d3fb4712ad53fe2fc1f06cfa853c1fd552d5a23b0d46fc8160925b62ed5ff09f19ee41d48309ba5ae55bce9eb1964c4b1f6ce65b25507f3615eb0da2fc824
data/bin/lace CHANGED
@@ -48,7 +48,7 @@ case ARGV.first when '-h', '--help', '--usage', '-?', 'help', nil
48
48
  require 'cmd/help'
49
49
  puts Lace.help_s
50
50
  exit ARGV.first ? 0 : 1
51
- when '--version'
51
+ when '--version', '-v'
52
52
  puts Lace::VERSION
53
53
  exit 0
54
54
  end
data/lib/cmd/apply.rb ADDED
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lace/package'
4
+ require 'lace/exceptions'
5
+ require 'fileutils'
6
+
7
+ module Lace
8
+ module_function
9
+
10
+ def apply
11
+ package_name = ARGV.shift
12
+ raise ResourceNotSpecified unless package_name
13
+ ohai "Applying package #{package_name}"
14
+ package = Package.new(package_name, ARGV.first)
15
+
16
+ diff = PackageUtils.diff(package_name)
17
+
18
+ if diff.added.empty? && diff.removed.empty?
19
+ puts 'Everything is already in sync. Nothing to do.'
20
+ return
21
+ end
22
+
23
+ diff.added.each do |f|
24
+ dest = f.as_dotfile(Dir.home, package.path)
25
+ dest_path = dest.to_path.gsub(Dir.home, '$HOME')
26
+ puts "Link #{f.basename} to #{dest_path}?"
27
+ next unless ARGV.confirm?("Link #{f.basename} to #{dest_path}?")
28
+
29
+ package.link_file_or_directory(f, dest, force: true)
30
+ ohai "Linked"
31
+ end
32
+
33
+ diff.removed.each do |f|
34
+ dest = f.as_dotfile(Dir.home, package.path)
35
+ dest_path = dest.to_path.gsub(Dir.home, '$HOME')
36
+ next unless ARGV.confirm?("Remove link for #{f.basename} from #{dest_path}?")
37
+
38
+ package.unlink_file(f, force: true)
39
+ ohai "Removed"
40
+ end
41
+ end
42
+ end
data/lib/cmd/diff.rb ADDED
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lace/package'
4
+ require 'lace/exceptions'
5
+
6
+ module Lace
7
+ module_function
8
+
9
+ def diff
10
+ package_name = ARGV.shift
11
+ raise ResourceNotSpecified unless package_name
12
+
13
+ package = Package.new(package_name, ARGV.first)
14
+ diff = PackageUtils.diff package_name
15
+
16
+ if diff.added.empty? && diff.removed.empty?
17
+ ohai 'Everything is already in sync.'
18
+ return
19
+ end
20
+
21
+ ohai 'The following files would be added [+] or removed [-]'
22
+ diff.added.each do |f|
23
+ puts "[+] #{f.basename} -> #{f.as_dotfile(Dir.home, package.path).to_path.gsub(Dir.home, '$HOME')}"
24
+ end
25
+ diff.removed.each do |f|
26
+ puts "[-] #{f.basename} -> #{f.as_dotfile(Dir.home, package.path).to_path.gsub(Dir.home, '$HOME')}"
27
+ end
28
+ end
29
+ end
data/lib/cmd/help.rb CHANGED
@@ -24,6 +24,7 @@ Troubleshooting:
24
24
  lace help
25
25
  lace info <pkg-name>
26
26
  lace validate <local-directory>
27
+ lace --version
27
28
 
28
29
  For further help visit:
29
30
  https://github.com/kairichard/lace
data/lib/cmd/setup.rb CHANGED
@@ -1,7 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'lace/package'
2
4
  require 'lace/exceptions'
3
5
 
4
- module Lace extend self
6
+ module Lace
7
+ module_function
8
+
5
9
  def setup
6
10
  package_name = ARGV.shift
7
11
  PackageUtils.setup package_name
data/lib/cmd/validate.rb CHANGED
@@ -1,28 +1,33 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'erb'
2
4
 
3
5
  require 'lace/package'
4
6
  require 'lace/utils'
5
7
  require 'lace/exceptions'
6
8
 
7
- VALIDATE = <<-EOS
8
- Lace-Manifest Validation Report:
9
- <% validation.errors.each do |error| -%>
10
- <%= "%-58s [ %s ]" % [error[0] + ':', error[1]] %>
11
- <% unless error[2].nil? -%>
12
- <% error[2].each do |line| -%>
13
- <%= Tty.gray %><%= '# '+line.to_s %><%= Tty.reset %>
14
- <% end -%>
15
- <% end -%>
16
- <% end -%>
9
+ VALIDATE = <<~EOS
10
+ Lace-Manifest Validation Report:
11
+ <% validation.errors.each do |error| -%>
12
+ <%= "%-58s [ %s ]" % [error[0] + ':', error[1]] %>
13
+ <% unless error[2].nil? -%>
14
+ <% error[2].each do |line| -%>
15
+ <%= Tty.gray %><%= '# '+line.to_s %><%= Tty.reset %>
16
+ <% end -%>
17
+ <% end -%>
18
+ <% end -%>
17
19
  EOS
18
20
 
19
- module Lace extend self
21
+ module Lace
22
+ module_function
23
+
20
24
  def validate
21
25
  resource = ARGV.shift
22
26
  flavor = ARGV.shift
23
27
  raise ResourceNotSpecified unless resource
28
+
24
29
  validation = PackageValidator.new(PackageFacts.new(resource), flavor)
25
- puts ERB.new(VALIDATE, nil, '-').result(binding)
30
+ puts ERB.new(VALIDATE, trim_mode: '-').result(binding)
26
31
  Lace.failed = true if validation.has_errors?
27
32
  end
28
33
  end
@@ -32,7 +37,8 @@ class PackageValidator
32
37
 
33
38
  class << self
34
39
  attr_accessor :validations
35
- def validate name, method_name
40
+
41
+ def validate(name, method_name)
36
42
  @validations ||= []
37
43
  @validations << [name, method_name]
38
44
  end
@@ -44,18 +50,19 @@ class PackageValidator
44
50
  validate 'setup', :setup_ok
45
51
  validate 'post-update hook', :post_update_hooks_ok
46
52
 
47
- def initialize facts, flavor=nil
53
+ def initialize(facts, flavor = nil)
48
54
  @facts = facts
49
55
  @errors = []
50
56
  if @facts.has_flavors? && flavor.nil?
51
- raise RuntimeError.new(FlavorArgumentMsg % @facts.flavors.join("\n- "))
57
+ raise FlavorArgumentMsg % @facts.flavors.join("\n- ").to_s
52
58
  elsif @facts.has_flavors? && flavor != false
53
59
  @facts.flavor! flavor
54
60
  end
61
+
55
62
  validate
56
63
  end
57
64
 
58
- def check_hooks hook_cmd
65
+ def check_hooks(hook_cmd)
59
66
  hook_cmd.map do |cmd|
60
67
  if !File.exist? cmd
61
68
  "#{cmd} cannot be found"
@@ -65,13 +72,13 @@ class PackageValidator
65
72
  end.compact
66
73
  end
67
74
 
68
- def hook_ok config_files
75
+ def hook_ok(config_files)
69
76
  hook_cmd = config_files
70
77
  if hook_cmd.empty?
71
78
  ["#{Tty.green}skipped#{Tty.reset}", nil]
72
79
  else
73
80
  errors = check_hooks hook_cmd
74
- if errors.length > 0
81
+ if errors.length.positive?
75
82
  ["#{Tty.red}error#{Tty.reset}", errors]
76
83
  else
77
84
  ["#{Tty.green}ok#{Tty.reset}", nil]
@@ -88,7 +95,7 @@ class PackageValidator
88
95
  end
89
96
 
90
97
  def homepage_present
91
- if @facts.has_key? 'homepage'
98
+ if @facts.key? 'homepage'
92
99
  ["#{Tty.green}found#{Tty.reset}", nil]
93
100
  else
94
101
  ["#{Tty.red}missing#{Tty.reset}", ['adding a homepage improves the credibility', 'of your package']]
@@ -96,7 +103,7 @@ class PackageValidator
96
103
  end
97
104
 
98
105
  def version_present
99
- if @facts.has_key? 'version'
106
+ if @facts.key? 'version'
100
107
  ["#{Tty.green}found#{Tty.reset}", nil]
101
108
  else
102
109
  ["#{Tty.red}missing#{Tty.reset}", ['adding a version to the manifest improves', 'a future update experince']]
@@ -106,8 +113,10 @@ class PackageValidator
106
113
  def config_files_present
107
114
  if @facts.config_files.empty?
108
115
  ["#{Tty.red}missing#{Tty.reset}", ['Add config_files see manual for more information']]
109
- elsif @facts.config_files.any?{|f| !File.exist? f}
110
- ["#{Tty.red}error#{Tty.reset}", @facts.config_files.select{|f| !File.exist? f}.map{|f| "#{f.to_s.split("/").last} is missing from this package"}]
116
+ elsif @facts.config_files.any? { |f| !File.exist? f }
117
+ ["#{Tty.red}error#{Tty.reset}", @facts.config_files.reject do |f|
118
+ File.exist? f
119
+ end.map { |f| "#{f.to_s.split('/').last} is missing from this package" }]
111
120
  else
112
121
  ["#{Tty.green}found#{Tty.reset}", nil]
113
122
  end
@@ -120,6 +129,6 @@ class PackageValidator
120
129
  end
121
130
 
122
131
  def has_errors?
123
- errors.any?{|e| !e[2].nil? }
132
+ errors.any? { |e| !e[2].nil? }
124
133
  end
125
134
  end
data/lib/extend/ARGV.rb CHANGED
@@ -78,6 +78,16 @@ module LaceArgvExtension
78
78
  end
79
79
  end
80
80
 
81
+ def confirm?(prompt)
82
+ if flag?('--yes')
83
+ true
84
+ else
85
+ print "#{prompt} [y/N] "
86
+ $stdout.flush
87
+ /y/i.match?(STDIN.gets)
88
+ end
89
+ end
90
+
81
91
  private
82
92
 
83
93
  def downcased_unique_named
@@ -1,8 +1,10 @@
1
- #Copyright 2009-2014 Max Howell and other contributors.
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2009-2014 Max Howell and other contributors.
2
4
  #
3
- #Redistribution and use in source and binary forms, with or without
4
- #modification, are permitted provided that the following conditions
5
- #are met:
5
+ # Redistribution and use in source and binary forms, with or without
6
+ # modification, are permitted provided that the following conditions
7
+ # are met:
6
8
  #
7
9
  # 1. Redistributions of source code must retain the above copyright
8
10
  # notice, this list of conditions and the following disclaimer.
@@ -10,45 +12,41 @@
10
12
  # notice, this list of conditions and the following disclaimer in the
11
13
  # documentation and/or other materials provided with the distribution.
12
14
  #
13
- #THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
14
- #IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
15
- #OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
16
- #IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
17
- #INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
18
- #NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
19
- #DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
20
- #THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
21
- #(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
22
- #THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
23
-
24
- require "fileutils"
15
+ # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
16
+ # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
17
+ # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
18
+ # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
19
+ # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
20
+ # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
21
+ # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
22
+ # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
24
+ # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25
+
26
+ require 'fileutils'
25
27
 
26
28
  class AbstractDownloadStrategy
27
- attr_reader :name, :resource, :target_folder
29
+ attr_reader :resource, :target_folder, :uri
28
30
 
29
- def initialize uri, desired_package_name=nil
31
+ def initialize(uri, desired_package_name = nil)
30
32
  @desired_package_name = desired_package_name
31
33
  @uri = uri
32
- @target_folder = Lace.pkgs_folder/name
33
- end
34
-
35
- def uri
36
- @uri
34
+ @target_folder = Lace.pkgs_folder / name
37
35
  end
38
36
 
39
37
  # All download strategies are expected to implement these methods
40
38
  def fetch; end
41
39
  def stage; end
40
+
42
41
  def name
43
42
  @desired_package_name
44
43
  end
45
44
  end
46
45
 
47
-
48
46
  class LocalFileStrategy < AbstractDownloadStrategy
49
47
  def fetch
50
- ohai "Fetching #@uri into #@target_folder"
51
- FileUtils.cp_r @uri, @target_folder, :preserve => true
48
+ ohai "Fetching #{@uri} into #{@target_folder}"
49
+ FileUtils.cp_r @uri, @target_folder, preserve: true
52
50
  @target_folder
53
51
  end
54
52
 
@@ -57,22 +55,21 @@ class LocalFileStrategy < AbstractDownloadStrategy
57
55
  end
58
56
  end
59
57
 
60
-
61
58
  module GitCommands
62
59
  def update_repo
63
60
  safe_system 'git', 'fetch', 'origin'
64
61
  end
65
62
 
66
63
  def reset
67
- safe_system 'git', "reset" , "--hard", "origin/HEAD"
64
+ safe_system 'git', 'reset', '--hard', 'origin/HEAD'
68
65
  end
69
66
 
70
67
  def git_dir
71
- @target_folder.join(".git")
68
+ @target_folder.join('.git')
72
69
  end
73
70
 
74
71
  def repo_valid?
75
- quiet_system "git", "--git-dir", git_dir, "status", "-s"
72
+ quiet_system 'git', '--git-dir', git_dir, 'status', '-s'
76
73
  end
77
74
 
78
75
  def repo_modified?
@@ -85,11 +82,11 @@ module GitCommands
85
82
  end
86
83
 
87
84
  def submodules?
88
- @target_folder.join(".gitmodules").exist?
85
+ @target_folder.join('.gitmodules').exist?
89
86
  end
90
87
 
91
88
  def clone_args
92
- args = %w{clone}
89
+ args = %w[clone]
93
90
  args << @uri << @target_folder
94
91
  end
95
92
 
@@ -103,46 +100,44 @@ module GitCommands
103
100
  end
104
101
  end
105
102
 
106
-
107
103
  class GitUpdateStrategy
108
104
  include GitCommands
109
105
 
110
- def initialize name
111
- @target_folder = Lace.pkgs_folder/name
106
+ def initialize(name)
107
+ @target_folder = Lace.pkgs_folder / name
112
108
  end
113
109
 
114
110
  def update
115
111
  if repo_valid?
116
- puts "Updating #@target_folder"
112
+ puts "Updating #{@target_folder}"
117
113
  @target_folder.cd do
118
114
  update_repo
119
115
  reset
120
116
  update_submodules if submodules?
121
117
  end
122
118
  else
123
- puts "Removing invalid .git repo"
119
+ puts 'Removing invalid .git repo'
124
120
  FileUtils.rm_rf @target_folder
125
121
  clone_repo
126
122
  end
127
123
  end
128
124
  end
129
125
 
130
-
131
126
  class GitDownloadStrategy < AbstractDownloadStrategy
132
127
  include GitCommands
133
128
 
134
129
  def fetch
135
- ohai "Cloning #@uri"
130
+ ohai "Cloning #{@uri}"
136
131
 
137
132
  if @target_folder.exist? && repo_valid?
138
- puts "Updating #@target_folder"
133
+ puts "Updating #{@target_folder}"
139
134
  @target_folder.cd do
140
135
  update_repo
141
136
  reset
142
137
  update_submodules if submodules?
143
138
  end
144
139
  elsif @target_folder.exist?
145
- puts "Removing invalid .git repo"
140
+ puts 'Removing invalid .git repo'
146
141
  FileUtils.rm_rf @target_folder
147
142
  clone_repo
148
143
  else
@@ -154,47 +149,45 @@ class GitDownloadStrategy < AbstractDownloadStrategy
154
149
  def name
155
150
  if super
156
151
  super
157
- elsif @uri.include? "github.com"
158
- @uri.split("/")[-2]
152
+ elsif @uri.include? 'github.com'
153
+ @uri.split('/')[-2]
159
154
  elsif File.directory? @uri
160
155
  File.basename(@uri)
161
156
  else
162
- raise "Cannot determine a proper name with #@uri"
157
+ raise "Cannot determine a proper name with #{@uri}"
163
158
  end
164
159
  end
165
-
166
160
  end
167
161
 
168
- class AbbrevGitDownloadStrategy < GitDownloadStrategy
169
- def initialize uri, desired_package_name=nil
170
- unless uri.end_with?(".git")
171
- uri = "#{uri}.git"
172
- end
162
+ class GitHubDownloadStrategy < GitDownloadStrategy
163
+ def initialize(uri, desired_package_name = nil)
164
+ uri = "#{uri}.git" unless uri.end_with?('.git')
173
165
  uri = "https://github.com/#{uri}"
174
- super uri, desired_package_name
166
+ super
175
167
  end
176
168
  end
177
169
 
178
-
179
170
  class DownloadStrategyDetector
180
171
  def self.detect(uri)
181
- detect_from_uri(uri)
172
+ detect_from_uri(uri)
182
173
  end
183
174
 
184
175
  def self.detect_from_uri(uri)
185
- is_git_dir = File.directory?(uri+"/.git")
186
- has_single_slash = uri.scan("/").count == 1
187
- if File.directory?(uri) && !is_git_dir
176
+ is_git_dir = File.directory?("#{uri}/.git")
177
+ has_single_slash = uri.scan('/').count == 1
178
+ via_ssh = uri.start_with?('git@')
179
+
180
+ if File.directory?(uri) && !is_git_dir && !via_ssh
188
181
  return LocalFileStrategy
189
182
  elsif is_git_dir
190
183
  return GitDownloadStrategy
191
- elsif has_single_slash
192
- return AbbrevGitDownloadStrategy
184
+ elsif has_single_slash && !via_ssh
185
+ return GitHubDownloadStrategy
193
186
  end
194
187
 
195
188
  case uri
196
- when %r[^git://] then GitDownloadStrategy
197
- when %r[^https?://.+\.git$] then GitDownloadStrategy
189
+ when /^git@/ then GitDownloadStrategy
190
+ when %r{^https?://.+\.git$} then GitDownloadStrategy
198
191
  # else CurlDownloadStrategy
199
192
  else
200
193
  raise "Cannot determine download startegy from #{uri}"
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'yaml'
2
4
  require 'erb'
3
5
 
@@ -8,17 +10,18 @@ class PackageFacts
8
10
  "#<Facts:#{@package_path}>"
9
11
  end
10
12
 
11
- def initialize package_path
13
+ def initialize(package_path)
12
14
  @package_path = Pathname.new(package_path)
13
- @facts_file = @package_path/".lace.yml"
14
- raise PackageFactsNotFound.new(@package_path) unless @facts_file.exist?
15
+ @facts_file = @package_path / '.lace.yml'
16
+ raise PackageFactsNotFound, @package_path unless @facts_file.exist?
17
+
15
18
  @facts = facts_file_to_hash
16
19
  @unflavorable_facts = facts_file_to_hash
17
20
  end
18
21
 
19
- def flavor_with the_flavor
22
+ def flavor_with(the_flavor)
20
23
  if has_flavors? && the_flavor.nil?
21
- raise FlavorArgumentRequired.new flavors
24
+ raise FlavorArgumentRequired, flavors
22
25
  elsif has_flavors? && the_flavor != false
23
26
  flavor! the_flavor
24
27
  end
@@ -26,78 +29,88 @@ class PackageFacts
26
29
 
27
30
  def config_files
28
31
  if has_config_files?
29
- @facts["config_files"].flatten.map do |path|
32
+ @facts['config_files'].flatten.map do |path|
30
33
  Pathname.glob(@package_path + path).delete_if do |match|
31
- match.directory? and path.include? "*"
34
+ match.directory? and path.include? '*'
32
35
  end
33
36
  end.flatten
34
- else [] end
37
+ else
38
+ [] end
39
+ end
40
+
41
+ def globbed_folder
42
+ if has_config_files?
43
+ @facts['config_files'].flatten.select { |f| f.include? '*' }
44
+ else
45
+ [] end
35
46
  end
36
47
 
37
48
  def has_config_files?
38
- has_key? 'config_files'
49
+ key? 'config_files'
39
50
  end
40
51
 
41
52
  def has_flavors?
42
- @unflavorable_facts && !@unflavorable_facts["flavors"].nil?
53
+ @unflavorable_facts && !@unflavorable_facts['flavors'].nil?
43
54
  end
44
55
 
45
- def has_key? key
46
- @unflavorable_facts && (@unflavorable_facts.has_key?(key) or @facts.has_key?(key))
56
+ def key?(key)
57
+ @unflavorable_facts && (@unflavorable_facts.key?(key) or @facts.key?(key))
47
58
  end
48
59
 
49
60
  def version
50
- @unflavorable_facts["version"] if @unflavorable_facts.key? "version"
61
+ @unflavorable_facts['version'] if @unflavorable_facts.key? 'version'
51
62
  end
52
63
 
53
64
  def setup_files
54
- @facts["setup"].flatten rescue []
65
+ @facts['setup'].flatten
66
+ rescue StandardError
67
+ []
55
68
  end
56
69
 
57
70
  def homepage
58
- @unflavorable_facts["homepage"] if @unflavorable_facts.key? "homepage"
71
+ @unflavorable_facts['homepage'] if @unflavorable_facts.key? 'homepage'
59
72
  end
60
73
 
61
74
  def flavors
62
- if @unflavorable_facts && @unflavorable_facts.key?("flavors")
63
- @unflavorable_facts["flavors"].keys.sort
75
+ if @unflavorable_facts&.key?('flavors')
76
+ @unflavorable_facts['flavors'].keys.sort
64
77
  else
65
78
  []
66
79
  end
67
80
  end
68
81
 
69
- def flavor! the_flavor
82
+ def flavor!(the_flavor)
70
83
  raise PackageFlavorDoesNotExist.new(the_flavor, flavors) unless flavors.include? the_flavor
71
- @facts = @unflavorable_facts["flavors"][the_flavor]
84
+
85
+ @facts = @unflavorable_facts['flavors'][the_flavor]
72
86
  end
73
87
 
74
88
  def unflavor!
75
89
  @facts = @unflavorable_facts
76
90
  end
77
91
 
78
- def post hook_point
79
- if @unflavorable_facts.nil? or !@facts.key? "post"
92
+ def post(hook_point)
93
+ if @unflavorable_facts.nil? || !@facts.key?('post')
80
94
  []
81
95
  else
82
- post_hook = @facts["post"]
96
+ post_hook = @facts['post']
83
97
  (post_hook[hook_point.to_s] || []).flatten
84
98
  end
85
99
  end
86
100
 
87
101
  protected
102
+
88
103
  def facts_file_to_hash
89
104
  begin
90
105
  rendered_manifest = ERB.new(@facts_file.read, trim_mode: '-').result(binding)
91
106
  rescue Exception => e
92
107
  raise ManifestErbError.new(self, e)
93
108
  end
94
- value = YAML.load rendered_manifest, aliases: true
95
- if value.is_a?(String) && value == "---"
96
- return Hash.new
109
+ value = YAML.safe_load rendered_manifest, aliases: true
110
+ if value.is_a?(String) && value == '---'
111
+ {}
97
112
  else
98
113
  value
99
114
  end
100
115
  end
101
116
  end
102
-
103
-
@@ -1,31 +1,35 @@
1
- require 'set'
1
+ # frozen_string_literal: true
2
2
 
3
+ require 'set'
3
4
  require 'lace/download_strategy'
4
5
 
5
6
  class Package
6
7
  include GitCommands
7
8
  attr_reader :name, :facts, :path
8
9
 
9
- def initialize name, flavor=nil
10
+ def initialize(name, flavor = nil)
10
11
  @name = name
11
- @path = Lace.pkgs_folder/name
12
- raise PackageNotInstalled.new(name) unless @path.exist?
12
+ @path = Lace.pkgs_folder / name
13
+ raise PackageNotInstalled, name unless @path.exist?
14
+
13
15
  @flavor = flavor
14
16
  read_facts!
15
17
  end
16
18
 
17
19
  def setup
18
20
  return if ARGV.nohooks?
21
+
19
22
  ENV['LACEPKG_PATH'] = @path
20
- @path.cd do
21
- facts.setup_files.each do |cmd|
22
- safe_system cmd
23
- end
24
- end
23
+ @path.cd do
24
+ facts.setup_files.each do |cmd|
25
+ safe_system cmd
26
+ end
27
+ end
25
28
  end
26
29
 
27
30
  def after_update
28
31
  return if ARGV.nohooks?
32
+
29
33
  @path.cd do
30
34
  facts.post(:update).each do |cmd|
31
35
  system cmd
@@ -40,17 +44,18 @@ class Package
40
44
 
41
45
  def is_modified?
42
46
  return false unless is_git_repo?
47
+
43
48
  @target_folder = @path
44
49
  repo_modified?
45
50
  end
46
51
 
47
52
  def is_active?
48
- home_dir = ENV["HOME"]
53
+ home_dir = Dir.home
49
54
  if @facts.has_flavors? && @flavor == false
50
- @facts.flavors.any?{|f| Package.new(@name, f).is_active?}
55
+ @facts.flavors.any? { |f| Package.new(@name, f).is_active? }
51
56
  else
52
57
  config_files = Set.new @facts.config_files
53
- config_files.all? do |p|
58
+ config_files.all? do |p|
54
59
  dotfile = p.as_dotfile(home_dir, @path)
55
60
  dotfile.exist? and dotfile.symlink? and dotfile.readlink.dirname.to_s.include?(@path)
56
61
  end
@@ -64,11 +69,8 @@ class Package
64
69
 
65
70
  def deactivate!
66
71
  files = @facts.config_files
67
- home_dir = ENV["HOME"]
68
72
  files.each do |file|
69
- file = Pathname.new(file)
70
- dotfile = file.as_dotfile(home_dir, @path)
71
- FileUtils.rm_f dotfile if dotfile.exist? && dotfile.readlink == file
73
+ unlink_file file
72
74
  end
73
75
  end
74
76
 
@@ -79,23 +81,30 @@ class Package
79
81
  end
80
82
  end
81
83
 
82
- def link_file file
83
- home_dir = ENV["HOME"]
84
+ def unlink_file(file, force: ARGV.force?)
85
+ dotfile = file.as_dotfile(Dir.home, @path)
86
+ if (dotfile.exist? || force) && dotfile.symlink? && dotfile.readlink == file
87
+ FileUtils.rm_f(dotfile)
88
+ return true
89
+ end
90
+ false
91
+ end
92
+
93
+ def link_file(file)
94
+ home_dir = Dir.home
84
95
  # if ends in erb -> generate it
85
96
  src = file
86
97
  dest = src.as_dotfile(home_dir, @path)
87
98
  if dest.exist? && dest.directory?
88
99
  raise WouldOverwriteError.new(dest, src) unless ARGV.force?
100
+
89
101
  FileUtils.mv dest, dest.as_backup
90
102
  end
91
- link_file_or_directory src, dest
103
+ link_file_or_directory src, dest, force: ARGV.force?
92
104
  end
93
105
 
94
- def link_file_or_directory(src, dest)
95
- puts "link_file"
96
- unless dest.dirname.exist?
97
- dest.dirname.mkpath
98
- end
99
- FileUtils.ln_s src, dest, :force => ARGV.force?
106
+ def link_file_or_directory(src, dest, force: ARGV.force?)
107
+ dest.dirname.mkpath unless dest.dirname.exist?
108
+ FileUtils.ln_s src, dest, force: force
100
109
  end
101
110
  end
@@ -1,59 +1,99 @@
1
- require 'lace/exceptions'
1
+ # frozen_string_literal: true
2
2
 
3
- class PackageUtils
3
+ require('lace/exceptions')
4
+
5
+ Diff = Struct.new(:added, :removed) do
6
+ end
4
7
 
5
- def self.fetch uri, desired_package_name=nil
8
+ class PackageUtils
9
+ def self.fetch(uri, desired_package_name = nil)
6
10
  downloader_cls = DownloadStrategyDetector.detect(uri)
7
11
  downloader = downloader_cls.new(uri, desired_package_name)
8
- raise PackageAlreadyInstalled.new(downloader.target_folder) if downloader.target_folder.exist?
12
+ raise(PackageAlreadyInstalled, downloader.target_folder) if downloader.target_folder.exist?
13
+
9
14
  downloader.fetch
10
- return downloader.name, downloader.target_folder
15
+ [downloader.name, downloader.target_folder]
11
16
  end
12
17
 
13
- def self.remove package_name
18
+ def self.remove(package_name)
14
19
  package = Package.new(package_name, false)
15
- raise CannotRemoveActivePackage.new if package.is_active?
16
- ohai "Removing"
17
- FileUtils.rm_rf package.path
18
- end
19
-
20
- def self.setup package_name
21
- begin
22
- package = Package.new(package_name, ARGV.first)
23
- package.activate!
24
- package.setup
25
- rescue FlavorError => e
26
- onoe e.message
27
- onoe "Package remains installed but was not activated"
28
- end
20
+ raise(CannotRemoveActivePackage) if package.is_active?
21
+
22
+ ohai('Removing')
23
+ FileUtils.rm_rf(package.path)
24
+ end
25
+
26
+ def self.setup(package_name)
27
+ package = Package.new(package_name, ARGV.first)
28
+ package.activate!
29
+ package.setup
30
+ rescue StandardError => e
31
+ onoe(e.message)
32
+ odie('Package remains installed but was not activated')
29
33
  end
30
34
 
31
- def self.deactivate package_name
35
+ def self.deactivate(package_name)
32
36
  package = Package.new(package_name, ARGV.first)
33
- raise NonActiveFlavorError.new unless package.is_active? || ARGV.force?
34
- ohai "Deactivating"
37
+ raise(NonActiveFlavorError) unless package.is_active? || ARGV.force?
38
+
39
+ ohai('Deactivating')
35
40
  package.deactivate!
36
41
  end
37
42
 
38
- def self.activate package_name
43
+ def self.activate(package_name)
39
44
  package = Package.new(package_name, ARGV.first)
40
- raise AlreadyActiveError.new if Package.new(package_name, false).is_active?
41
- ohai "Activating"
45
+ raise(AlreadyActiveError) if Package.new(package_name, false).is_active?
46
+
47
+ ohai('Activating')
42
48
  package.activate!
43
49
  end
44
50
 
45
- def self.update package_name
51
+ def self.update(package_name)
46
52
  package = Package.new(package_name, false)
47
- raise OnlyGitReposCanBeUpdatedError.new unless package.is_git_repo?
53
+ raise(OnlyGitReposCanBeUpdatedError) unless package.is_git_repo?
54
+
48
55
  updater = GitUpdateStrategy.new(package_name)
49
56
  was_active_before_update = package.is_active?
50
- self.deactivate(package_name) if was_active_before_update
51
- ohai "Updating"
57
+ deactivate(package_name) if was_active_before_update
58
+ ohai('Updating')
52
59
  updater.update
53
- self.activate(package_name) if was_active_before_update
54
- package = Package.new package_name, false
60
+ activate(package_name) if was_active_before_update
61
+ package = Package.new(package_name, false)
55
62
  package.after_update
56
63
  end
64
+
65
+ def self.diff(package_name)
66
+ home_dir = Dir.home
67
+ package = Package.new(package_name, ARGV.first)
68
+ config_files = Set.new(package.facts.config_files)
69
+ files_pointing_to_package = symlinks_to_package(package)
70
+ files_from_manifest_not_in_home = Set.new(config_files.reject do |f|
71
+ f.as_dotfile(home_dir, package.path).exist?
72
+ end)
73
+ Diff.new(files_from_manifest_not_in_home, (files_pointing_to_package - config_files))
74
+ end
75
+
76
+ def self.symlinks_to_package(package)
77
+ home_dir = Dir.home
78
+ found_links = Set.new
79
+ traverse_directory(home_dir, package) { |entry| (found_links << Pathname.new(File.readlink(entry))) }
80
+ found_links
81
+ end
57
82
  end
58
83
 
84
+ COMMON_CONFIG_FOLDERS = ['config'].freeze
85
+
86
+ def traverse_directory(directory, package, &block)
87
+ package_path = package.path
88
+ whitelisted_folders = package.facts.globbed_folder + COMMON_CONFIG_FOLDERS
89
+ Dir.foreach(directory) do |entry|
90
+ next if ['.', '..'].include?(entry)
59
91
 
92
+ entry_path = File.join(directory, entry)
93
+ if File.symlink?(entry_path) && File.readlink(entry_path).include?(package_path)
94
+ block.call(entry_path)
95
+ elsif File.directory?(entry_path) && whitelisted_folders.any? { |f| entry_path.include?(f) }
96
+ traverse_directory(entry_path, package, &block)
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lace/package'
4
+ require 'lace/exceptions'
data/lib/lace/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Lace
2
- VERSION = "0.5.1"
2
+ VERSION = "0.5.3"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lace
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.5.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kai Richard Koenig
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-12-13 00:00:00.000000000 Z
11
+ date: 2025-06-17 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Lace lets you manage your dotfiles when using them on multiple machines
14
14
  email: kai.richard.koenig@gmail.com
@@ -19,7 +19,9 @@ extra_rdoc_files: []
19
19
  files:
20
20
  - bin/lace
21
21
  - lib/cmd/activate.rb
22
+ - lib/cmd/apply.rb
22
23
  - lib/cmd/deactivate.rb
24
+ - lib/cmd/diff.rb
23
25
  - lib/cmd/fetch.rb
24
26
  - lib/cmd/help.rb
25
27
  - lib/cmd/inspect.rb
@@ -39,6 +41,7 @@ files:
39
41
  - lib/lace/package/facts.rb
40
42
  - lib/lace/package/package.rb
41
43
  - lib/lace/package/utils.rb
44
+ - lib/lace/package_cli.rb
42
45
  - lib/lace/utils.rb
43
46
  - lib/lace/version.rb
44
47
  homepage: https://github.com/kairichard/lace