confgit 0.0.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.
@@ -0,0 +1,614 @@
1
+ # coding: UTF-8
2
+
3
+ require 'fileutils'
4
+ require 'pathname'
5
+ require 'etc'
6
+ require 'shellwords'
7
+ require 'open3'
8
+
9
+ require 'rubygems'
10
+ require 'json'
11
+
12
+ require 'confgit/with_color'
13
+
14
+
15
+ module Confgit
16
+
17
+ class Repo
18
+ ROOT_KEY = 'confgit.root'
19
+
20
+ include WithColor
21
+
22
+ def initialize(path = '~/.etc/confgit')
23
+ @base_path = File.expand_path(path)
24
+ @repos_path = File.join(@base_path, 'repos')
25
+
26
+ FileUtils.mkpath(@repos_path)
27
+
28
+ @config = read_config(File.join(@base_path, 'confgit.conf'))
29
+ @repo_path = File.expand_path('current', @repos_path)
30
+
31
+ valid_repo unless File.symlink?(@repo_path)
32
+ end
33
+
34
+ # ホスト名
35
+ def hostname
36
+ `hostname`.chomp
37
+ end
38
+
39
+ # カレントリポジトリがない場合の処理
40
+ def valid_repo
41
+ repo = nil
42
+
43
+ repo_each { |file, is_current|
44
+ repo = file
45
+ break
46
+ }
47
+
48
+ chrepo(repo || hostname)
49
+ end
50
+
51
+ # リポジトリの変更
52
+ def chrepo(repo)
53
+ Dir.chdir(@repos_path) { |path|
54
+ begin
55
+ if File.symlink?('current')
56
+ return if File.readlink('current') == repo
57
+ File.unlink('current')
58
+ end
59
+
60
+ unless File.exist?(repo)
61
+ FileUtils.mkpath(repo)
62
+
63
+ Dir.chdir(repo) { |path|
64
+ begin
65
+ out, err, status = Open3.capture3('git', 'init')
66
+ $stderr.puts err unless err.empty?
67
+ rescue => e
68
+ FileUtils.remove_entry_secure(repo)
69
+ abort e.to_s
70
+ end
71
+ }
72
+ end
73
+
74
+ File.symlink(repo, 'current')
75
+ rescue => e
76
+ abort e.to_s
77
+ end
78
+ }
79
+ end
80
+
81
+ # リポジトリの削除
82
+ def rmrepo(repo, force = false)
83
+ Dir.chdir(@repos_path) { |path|
84
+ begin
85
+ if File.symlink?('current') && File.readlink('current') == repo
86
+ abort "'#{repo}' is current repository!" unless force
87
+ File.unlink('current')
88
+ end
89
+
90
+ FileUtils.remove_entry_secure(repo)
91
+
92
+ valid_repo unless File.symlink?('current')
93
+ rescue => e
94
+ abort e.to_s
95
+ end
96
+ }
97
+ end
98
+
99
+ # 設定の初期値
100
+ def default_config
101
+ {}
102
+ end
103
+
104
+ # 設定の読込み
105
+ def read_config(file)
106
+ if File.exist?(file)
107
+ config = JSON.parse(File.read(file))
108
+ config = default_config.merge(config)
109
+ else
110
+ config = default_config
111
+ File.write(file, JSON.pretty_generate(config)+"\n")
112
+ end
113
+
114
+ return config
115
+ end
116
+
117
+ # 外部コマンドを定義する
118
+ def self.define_command(command, *opts)
119
+ define_method "confgit_#{command}" do |options, *args|
120
+ args = getargs(args)
121
+
122
+ Dir.chdir(@repo_path) { |path|
123
+ begin
124
+ args = opts + args
125
+ args.push(options)
126
+ system_(command, *args)
127
+ rescue => e
128
+ abort e.to_s
129
+ end
130
+ }
131
+ end
132
+ end
133
+
134
+ # メソッドがない場合
135
+ def method_missing(name, *args, &block)
136
+ if name.to_s =~ /^confgit_(.+)$/
137
+ options = args.shift
138
+ args = git_args(args).push(options)
139
+
140
+ command = $1.gsub(/_/, '-')
141
+ git(command, *args)
142
+
143
+ # abort "#{CMD} '#{$'}' is not a git command. See '#{CMD} --help'.\n"
144
+ else
145
+ super
146
+ end
147
+ end
148
+
149
+ # 引数を利用可能にする
150
+ def getargs(args, force = false)
151
+ args.collect { |x|
152
+ run = false
153
+
154
+ case x
155
+ when /^-/
156
+ when /\//
157
+ run = true
158
+ else
159
+ run = force
160
+ end
161
+
162
+ if run
163
+ repo = File.realpath(@repo_path)
164
+ path = File.join(repo, x)
165
+ x = Pathname(path).relative_path_from(Pathname(repo)).to_s
166
+ end
167
+
168
+ x
169
+ }
170
+ end
171
+
172
+ # 引数の最後が Hash ならオプションとして取出す
173
+ def arg_last_options(args)
174
+ if args.last && args.last.kind_of?(Hash)
175
+ args.pop
176
+ else
177
+ {}
178
+ end
179
+ end
180
+
181
+ # オプションに応じて外部呼出しを行う
182
+ def system_(command, *args)
183
+ options = arg_last_options(args)
184
+
185
+ if options[:interactive] == false
186
+ out, err, status = Open3.capture3(command, *args)
187
+
188
+ $stdout.print out unless out.empty?
189
+ $stderr.print err unless err.empty?
190
+
191
+ status
192
+ elsif options[:capture]
193
+ Open3.capture3(command, *args)
194
+ else
195
+ system(command, *args)
196
+ end
197
+ end
198
+
199
+ # git を呼出す
200
+ def git(*args)
201
+ Dir.chdir(@repo_path) { |path|
202
+ begin
203
+ system_('git', *args)
204
+ rescue => e
205
+ abort e.to_s
206
+ end
207
+ }
208
+ end
209
+
210
+ # git コマンドの引数を生成する
211
+ def git_args(args)
212
+ args.collect { |item|
213
+ item = $' if item.kind_of?(String) && %r|^/| =~ item
214
+ item
215
+ }
216
+ end
217
+
218
+ # ルートのパスを取得する
219
+ def root
220
+ return @root if @root
221
+
222
+ # 表示
223
+ out, err, status = git('config', '--path', '--local', ROOT_KEY, :capture => true)
224
+ out.chomp!
225
+ out = '/' if out.empty?
226
+
227
+ @root = out
228
+ end
229
+
230
+ # ルートのパスを設定する
231
+ def root=(value)
232
+ if value && ! value.empty?
233
+ git('config', '--path', '--local', ROOT_KEY, value)
234
+ else
235
+ git('config', '--unset', '--local', ROOT_KEY)
236
+ end
237
+
238
+ @root = nil
239
+ end
240
+
241
+ # ルートからの相対パス
242
+ def relative_path(path)
243
+ root_path = Pathname.new(root)
244
+ Pathname.new(path).relative_path_from(root_path).to_s
245
+ end
246
+
247
+ # ファイルの hash値を求める
248
+ def hash_object(file)
249
+ path = File.expand_path(file)
250
+ open("| git hash-object \"#{path}\"") {|f|
251
+ return f.gets.chomp
252
+ }
253
+ end
254
+
255
+ # 確認プロンプトを表示する
256
+ def yes?(prompt, y = true)
257
+ yn = y ? 'Yn' : 'yN'
258
+ print "#{prompt} [#{yn}]: "
259
+
260
+ result = $stdin.gets.chomp
261
+
262
+ return y if result.empty?
263
+
264
+ if /^(y|yes)$/i =~ result
265
+ y = true
266
+ else
267
+ y = false
268
+ end
269
+
270
+ y
271
+ end
272
+
273
+ # ファイルのコピー(属性は維持する)
274
+ def filecopy(from, to, exiting = false)
275
+ begin
276
+ to_dir = File.dirname(to)
277
+ FileUtils.mkpath(to_dir)
278
+
279
+ if File.exist?(to) && ! File.writable_real?(to)
280
+ # 書込みできない場合は削除を試みる
281
+ File.unlink(to)
282
+ end
283
+
284
+ FileUtils.copy(from, to)
285
+ stat = File.stat(from)
286
+ File.utime(stat.atime, stat.mtime, to)
287
+ File.chmod(stat.mode, to)
288
+
289
+ return true
290
+ rescue => e
291
+ abort e.to_s if exiting
292
+ $stderr.puts e.to_s
293
+ end
294
+ end
295
+
296
+ # ディレクトリ内のファイルを繰返す
297
+ def dir_each(subdir = '.')
298
+ Dir.chdir(File.expand_path(subdir, @repo_path)) { |path|
299
+ Dir.foreach('.') { |file|
300
+ next if /^(\.git|\.$|\.\.$)/ =~ file
301
+
302
+ yield(file)
303
+
304
+ if File.directory?(file)
305
+ Dir.glob("#{file}/**/*", File::FNM_DOTMATCH) { |file|
306
+ if /(^|\/)(\.git|\.|\.\.)$/ !~ file
307
+ yield(file)
308
+ end
309
+ }
310
+ end
311
+ }
312
+ }
313
+ end
314
+
315
+ # git に管理されているファイルを繰返す
316
+ def git_each(*args)
317
+ args = getargs(args, true)
318
+ files = args.collect { |f| f.shellescape }
319
+
320
+ Dir.chdir(@repo_path) { |path|
321
+ open("| git ls-files --stage --full-name " + files.join(' ')) {|f|
322
+ while line = f.gets
323
+ mode, hash, stage, file = line.split
324
+
325
+ # file = line.chomp
326
+ next if /^\.git/ =~ file
327
+ next if File.directory?(file)
328
+
329
+ yield(file, hash)
330
+ end
331
+ }
332
+ }
333
+ end
334
+
335
+ # リポジトリを繰返す
336
+ def repo_each
337
+ Dir.chdir(@repos_path) { |path|
338
+ begin
339
+ current = File.expand_path(File.readlink('current'))
340
+ rescue
341
+ end
342
+
343
+ Dir.glob('*') { |file|
344
+ next if /^current$/ =~ file
345
+
346
+ if current && File.realpath(file) == current
347
+ is_current = true
348
+ current = nil
349
+ else
350
+ is_current = false
351
+ end
352
+
353
+ yield(file, is_current)
354
+ }
355
+
356
+ yield(File.readlink('current'), true) if current
357
+ }
358
+ end
359
+
360
+ # パスを展開する
361
+ def expand_path(path, dir = nil)
362
+ File.expand_path(path, dir).gsub(%r|^/private(/[^/]+)|) { |m|
363
+ begin
364
+ subdir = $1
365
+ m = subdir if File.realpath(subdir) == m
366
+ rescue
367
+ end
368
+
369
+ m
370
+ }
371
+ end
372
+
373
+ # オプションを取出す
374
+ def getopts(args)
375
+ options = []
376
+ args.each { |opt|
377
+ break unless /^-/ =~ opt
378
+ options << args.shift
379
+ }
380
+
381
+ options
382
+ end
383
+
384
+ # ファイルの更新チェック
385
+ def modfile?(from, to)
386
+ ! File.exist?(to) || hash_object(from) != hash_object(to)
387
+ end
388
+
389
+ # ファイル属性を文字列にする
390
+ def mode2str(bits)
391
+ case bits & 0170000 # S_IFMT
392
+ when 0010000 # S_IFIFO パイプ
393
+ mode = 'p'
394
+ when 0020000 # S_IFCHR キャラクタ・デバイス
395
+ mode = 'c'
396
+ when 0040000 # S_IFDIR ディレクトリ
397
+ mode = 'd'
398
+ when 0060000 # S_IFBLK ブロック・デバイス
399
+ mode = 'b'
400
+ when 0100000 # S_IFREG 通常ファイル
401
+ mode = '-'
402
+ when 0120000 # S_IFLNK シンボリックリンク
403
+ mode = 'l'
404
+ when 0140000 # S_IFSOCK ソケット
405
+ mode = 's'
406
+ when 0160000 # S_IFWHT BSD空白ファイル
407
+ mode = 'w'
408
+ end
409
+
410
+ mode += 'rwx'*3
411
+
412
+ (0..8).each { |i|
413
+ mask = 1<<i
414
+ mode[-(i+1)] = '-' if (bits & mask) == 0
415
+ }
416
+
417
+ if (bits & 0001000) != 0 # S_ISVTX スティッキービット
418
+ if mode[-1] == '-'
419
+ mode[-1] = 'T'
420
+ else
421
+ mode[-1] = 't'
422
+ end
423
+ end
424
+
425
+ mode
426
+ end
427
+
428
+ # コマンド
429
+
430
+ # カレントリポジトリの表示・変更
431
+ def confgit_repo(options, repo = nil)
432
+ if repo
433
+ # 変更
434
+ if options[:remove]
435
+ rmrepo(repo, options[:force])
436
+ else
437
+ chrepo(repo)
438
+ end
439
+ else
440
+ # 表示
441
+ repo_each { |file, is_current|
442
+ mark = is_current ? '*' : ' '
443
+ print "#{mark} #{file}\n"
444
+ }
445
+ end
446
+ end
447
+
448
+ # ルートの表示・変更
449
+ def confgit_root(options, value = nil)
450
+ if options[:remove]
451
+ # 削除
452
+ self.root = nil
453
+ elsif value
454
+ # 変更
455
+ self.root = value
456
+ else
457
+ # 表示
458
+ puts root
459
+ end
460
+ end
461
+
462
+ # リポジトリの初期化
463
+ def confgit_init(options)
464
+ FileUtils.mkpath(@repo_path)
465
+ git('init')
466
+ end
467
+
468
+ # ファイルを管理対象に追加
469
+ def confgit_add(options, *files)
470
+ confgit_init unless File.exist?(@repo_path)
471
+ repo = File.realpath(@repo_path)
472
+
473
+ files.each { |path|
474
+ path = expand_path(path)
475
+
476
+ if relative_path(path) =~ /^[.]{2}/
477
+ $stderr.puts "'#{path}' is outside directory"
478
+ next
479
+ end
480
+
481
+ if File.directory?(path)
482
+ dir_each(path) { |file|
483
+ next if File.directory?(file)
484
+
485
+ from = File.join(path, file)
486
+ to = File.join(repo, relative_path(from))
487
+
488
+ if filecopy(from, to)
489
+ git('add', to)
490
+ end
491
+ }
492
+ else
493
+ from = path
494
+ to = File.join(repo, relative_path(from))
495
+
496
+ if filecopy(from, to)
497
+ git('add', to)
498
+ end
499
+ end
500
+ }
501
+ end
502
+
503
+ # ファイルを管理対象から削除
504
+ def confgit_rm(options, *args)
505
+ return unless File.exist?(@repo_path)
506
+
507
+ options = getopts(args)
508
+ repo = File.realpath(@repo_path)
509
+
510
+ files = args.collect { |from|
511
+ File.join(repo, relative_path(expand_path(from)))
512
+ }
513
+
514
+ git('rm', *(options + files), :interactive => false)
515
+ end
516
+
517
+ # バックアップする
518
+ def confgit_backup(options, *args)
519
+ git_each(*args) { |file, hash|
520
+ next if File.directory?(file)
521
+
522
+ from = File.join(root, file)
523
+ to = File.join(@repo_path, file)
524
+
525
+ unless File.exist?(from)
526
+ with_color(:fg_red) { print "[?] #{file}" }
527
+ puts
528
+ next
529
+ end
530
+
531
+ if options[:force] || modfile?(from, to)
532
+ with_color(:fg_blue) { print "--> #{file}" }
533
+ write = options[:yes]
534
+
535
+ if write == nil
536
+ # 書込みが決定していない場合
537
+ write = yes?(nil, false)
538
+ else
539
+ puts
540
+ end
541
+
542
+ filecopy(from, to) if write
543
+ end
544
+ }
545
+
546
+ git('status', :interactive => false)
547
+ end
548
+
549
+ # リストアする
550
+ def confgit_restore(options, *args)
551
+ git_each(*args) { |file, hash|
552
+ next if File.directory?(file)
553
+
554
+ from = File.join(@repo_path, file)
555
+ to = File.join(root, file)
556
+
557
+ unless File.exist?(from)
558
+ with_color(:fg_red) { print "[?] #{file}" }
559
+ puts
560
+ next
561
+ end
562
+
563
+ if options[:force] || modfile?(from, to)
564
+ color = File.writable_real?(to) ? :fg_blue : :fg_magenta
565
+ with_color(color) { print "<-- #{file}" }
566
+ write = options[:yes]
567
+
568
+ if write == nil
569
+ # 書込みが決定していない場合
570
+ write = yes?(nil, false)
571
+ else
572
+ puts
573
+ end
574
+
575
+ filecopy(from, to) if write
576
+ end
577
+ }
578
+ end
579
+
580
+ # 一覧表示する
581
+ def confgit_list(options, *args)
582
+ git_each(*args) { |file, hash|
583
+ next if File.directory?(file)
584
+
585
+ from = File.join(root, file)
586
+ to = File.join(@repo_path, file)
587
+
588
+ if File.exist?(from)
589
+ stat = File.stat(from)
590
+ mode = options[:octal] ? stat.mode.to_s(8) : mode2str(stat.mode)
591
+ user = Etc.getpwuid(stat.uid).name
592
+ group = Etc.getgrgid(stat.gid).name
593
+ else
594
+ mode = ' ' * (options[:octal] ? 6 : 10)
595
+ user = '-'
596
+ group = '-'
597
+ end
598
+
599
+ print "#{mode}\t#{user}\t#{group}\t#{from}\n"
600
+ }
601
+ end
602
+
603
+ # リポジトリのパスを表示
604
+ def confgit_path(options, subdir = '.')
605
+ path = File.realpath(File.expand_path(subdir, @repo_path))
606
+ print path, "\n"
607
+ end
608
+
609
+ # 外部コマンド
610
+ define_command('tree', '-I', '.git') # tree表示する
611
+ define_command('tig') # tigで表示する
612
+ end
613
+
614
+ end
@@ -0,0 +1,4 @@
1
+ module Confgit
2
+ LONG_VERSION = File.read(File.expand_path('../../../VERSION', __FILE__)).chomp
3
+ VERSION = LONG_VERSION.gsub(/-.*$/, '')
4
+ end
@@ -0,0 +1,54 @@
1
+ # coding: UTF-8
2
+
3
+ module Confgit
4
+
5
+ module WithColor
6
+ ESC_CODES = {
7
+ # Text attributes
8
+ :clear => 0,
9
+ :bold => 1,
10
+ :underscore => 4,
11
+ :blink => 5,
12
+ :reverse => 7,
13
+ :concealed => 8,
14
+
15
+ # Foreground colors
16
+ :fg_black => 30,
17
+ :fg_red => 31,
18
+ :fg_green => 32,
19
+ :fg_yellow => 33,
20
+ :fg_blue => 34,
21
+ :fg_magenta => 35,
22
+ :fg_Cyan => 36,
23
+ :fg_White => 37,
24
+
25
+ # Background colors
26
+ :bg_black => 40,
27
+ :bg_red => 41,
28
+ :bg_green => 42,
29
+ :bg_yellow => 43,
30
+ :bg_blue => 44,
31
+ :bg_magenta => 45,
32
+ :bg_Cyan => 46,
33
+ :bg_White => 47,
34
+ }
35
+
36
+ # エスケープシーケンスをセットする
37
+ def set_color(*colors)
38
+ colors.each { |color|
39
+ print "\e[", ESC_CODES[color], "m"
40
+ }
41
+ end
42
+
43
+ # カラー表示する
44
+ def with_color(*colors)
45
+ begin
46
+ set_color(*colors)
47
+ yield
48
+ ensure
49
+ set_color(0)
50
+ end
51
+ end
52
+ end
53
+
54
+ end
data/lib/confgit.rb ADDED
@@ -0,0 +1,10 @@
1
+ require "confgit/version"
2
+ require "confgit/repo"
3
+ require "confgit/cli"
4
+
5
+
6
+ module Confgit
7
+ def self.run(argv = ARGV, options = {})
8
+ CLI.run(argv, options)
9
+ end
10
+ end