conjoiner 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,87 @@
1
+ # Conjoiner
2
+
3
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/conjoiner`. To experiment with that code, run `bin/console` for an interactive prompt.
4
+
5
+ TODO: Delete this and the text above, and describe your gem
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'conjoiner'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle install
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install conjoiner
22
+
23
+ ## Usage
24
+
25
+ After installing you can initialize the `prime` aspect with:
26
+
27
+ ```
28
+ conjoiner init_aspect
29
+ ```
30
+
31
+ This will create the dated repositories (gpx, log, misc) for the current year
32
+ and the `prime` aspect.
33
+
34
+ You can then manually synchronize those repositories with:
35
+ ```
36
+ conjoiner sync
37
+ ```
38
+
39
+ And show the status of the repositories with:
40
+ ```
41
+ conjoiner show
42
+ ```
43
+
44
+ ### Enable the synchronization deaemon on with systemd on Linux
45
+
46
+ You can enable the automatic synchronization with the following script:
47
+ ```
48
+ mkdir -p ~/.config/systemd/user
49
+ cp conjoiner.service ~/.config/systemd/user
50
+ systemctl --user daemon-reload
51
+ systemctl --user enable conjoiner
52
+ systemctl --user start conjoiner
53
+ ```
54
+
55
+ You can the use journalctl to view the logging with:
56
+ ```
57
+ journalctl --user --unit=conjoiner --follow
58
+ ```
59
+
60
+ ### Enable the synchronization deaemon on with launchd on OSX
61
+
62
+ You can enable the automatic synchronization with the following script:
63
+ ```
64
+ mkdir -p ~/Library/LaunchAgents
65
+ cp ca.andrewsullivancant.conjoiner.plist ~/Library/LaunchAgents
66
+ launchctl load ~/Library/LaunchAgents/ca.andrewsullivancant.conjoiner.plist
67
+ ```
68
+
69
+ You should be able to see logging with `Console.app`.
70
+
71
+
72
+ ## Development
73
+
74
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
75
+
76
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
77
+
78
+ ## Contributing
79
+
80
+ Bug reports and merge requests are welcome on Gitlab at https://gitlab.com/acant/conjoiner.
81
+
82
+ ## License
83
+
84
+ Released under the [AGPLv3 license](https://www.gnu.org/licenses/agpl-3.0.en.html).
85
+
86
+ Copyright 2021 [Andrew Sullivan Cant](https://andrewsullivancant.ca)
87
+
data/Rakefile ADDED
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+
5
+ require 'rspec/core/rake_task'
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require 'rubocop/rake_task'
9
+ RuboCop::RakeTask.new(:rubocop) do |task|
10
+ FileUtils.mkdir_p('tmp')
11
+ task.options = %w[--format progress --format simple --out tmp/rubocop.txt]
12
+ end
13
+
14
+ require 'cucumber/rake/task'
15
+ Cucumber::Rake::Task.new
16
+
17
+ require 'yard'
18
+ YARD::Rake::YardocTask.new do |t|
19
+ t.files = ['lib/**/*.rb']
20
+ t.stats_options = ['--list-undoc']
21
+ end
22
+
23
+ require 'bundler/audit/task'
24
+ Bundler::Audit::Task.new
25
+
26
+ task default: %i[spec cucumber rubocop yard]
data/bin/console ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # frozen_string_literal: true
4
+
5
+ require 'bundler/setup'
6
+ require 'conjoiner'
7
+
8
+ require 'irb'
9
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle config set path 'vendor/bundle'
7
+ bundle install
8
+
9
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,23 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+
6
+ <key>Label</key>
7
+ <string>ca.andrewsullivancant.conjoiner</string>
8
+
9
+ <key>RunAtLoad</key>
10
+ <true/>
11
+
12
+ <key>KeepAlive</key>
13
+ <true/>
14
+
15
+ <key>ProgramArguments</key>
16
+ <array>
17
+ <string>/usr/bin/ruby</string>
18
+ <string>/usr/local/bin/conjoiner</string>
19
+ <string>syncer</string>
20
+ </array>
21
+
22
+ </dict>
23
+ </plist>
data/conjoiner.gemspec ADDED
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/conjoiner/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'conjoiner'
7
+ spec.version = Conjoiner::VERSION
8
+ spec.authors = ['Andrew Sullivan Cant']
9
+ spec.email = ['mail@andrewsullivancant.ca']
10
+ spec.summary = 'Tool for cloning and managing git repositories.'
11
+ spec.description = 'Manager for cloned git reporitories, automatically sync some of those repositories.'
12
+ spec.homepage = 'https://gitlab.com/acant/conjoiner'
13
+ spec.metadata = {
14
+ 'homepage_uri' => spec.homepage,
15
+ 'source_code_uri' => spec.homepage,
16
+ 'changelog_uri' => "#{spec.homepage}/CHANGELOG.md"
17
+ }
18
+ spec.license = 'AGPL-3.0-or-later'
19
+ spec.files =
20
+ Dir.chdir(File.expand_path(__dir__)) do
21
+ `git ls-files -z`.split("\x0").reject do |f|
22
+ f.match(%r{^(test|spec|features)/})
23
+ end
24
+ end
25
+ spec.bindir = 'exe'
26
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
+ spec.require_paths = ['lib']
28
+
29
+ spec.required_ruby_version = Gem::Requirement.new('>= 2.5.0')
30
+
31
+ spec.add_dependency 'git', '~> 1.9'
32
+ spec.add_dependency 'listen', '~> 3.6'
33
+ spec.add_dependency 'table_print', '~> 1.5'
34
+ spec.add_dependency 'thor', '~> 1.0'
35
+
36
+ spec.add_development_dependency 'aruba', '~> 1.0'
37
+ spec.add_development_dependency 'cucumber', '~> 5.3'
38
+ spec.add_development_dependency 'rspec', '~> 3.10'
39
+ spec.add_development_dependency 'simplecov', '~> 0.21'
40
+
41
+ spec.add_development_dependency 'bundler-audit'
42
+ spec.add_development_dependency 'rubocop'
43
+ spec.add_development_dependency 'rubocop-performance'
44
+ spec.add_development_dependency 'rubocop-rake'
45
+ spec.add_development_dependency 'rubocop-rspec'
46
+ spec.add_development_dependency 'yard'
47
+ end
data/conjoiner.service ADDED
@@ -0,0 +1,9 @@
1
+ [Unit]
2
+ Description=Conjoiner synchronization
3
+
4
+ [Install]
5
+ WantedBy=default.target
6
+
7
+ [Service]
8
+ Restart=always
9
+ ExecStart=/usr/bin/ruby /usr/local/bin/conjoiner syncer
data/cucumber.yml ADDED
@@ -0,0 +1 @@
1
+ default: --publish-quiet
data/exe/conjoiner ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # frozen_string_literal: true
4
+
5
+ require 'conjoiner'
6
+
7
+ Conjoiner::CLI.start(ARGV)
@@ -0,0 +1,265 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require 'git'
5
+ require 'pathname'
6
+ require 'table_print'
7
+ require 'ostruct'
8
+ require 'listen'
9
+
10
+ # HACK: Disable various length and complexity checks temporarily.
11
+ # As I extract methods/class from CLI I will re-enable them.
12
+ # rubocop:disable Metrics/ClassLength, Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity, Metrics/BlockLength
13
+
14
+ module Conjoiner
15
+ class CLI < Thor # rubocop:disable Style/Documentation
16
+ # TODO: desc 'join URL', 'clone the git repository into conjoiner'
17
+
18
+ desc 'init_aspect [ASPECT] [YEAR]', 'initialize aspect for a year'
19
+ options year: :integer
20
+ def init_aspect(aspect = nil, year = nil)
21
+ aspect ||= 'prime'
22
+ year ||= Time.now.year
23
+
24
+ puts "init aspect #{aspect} #{year}"
25
+
26
+ home = Pathname.new(Dir.home)
27
+ dated_root = home.join(
28
+ 'joined',
29
+ 'git.andrewcant.ca',
30
+ 'andrew',
31
+ 'aspects',
32
+ aspect,
33
+ 'dated',
34
+ year.to_s
35
+ )
36
+
37
+ repository_names =
38
+ if aspect == 'prime'
39
+ %w[gpx log misc]
40
+ else
41
+ %w[log]
42
+ end
43
+
44
+ repository_names.each do |name|
45
+ repo = "gitolite@git.andrewcant.ca:andrew/aspects/#{aspect}/dated/#{year}/#{name}"
46
+ dir = dated_root.join(name)
47
+
48
+ puts repo
49
+ puts dir
50
+
51
+ git =
52
+ if dir.exist?
53
+ begin
54
+ git_open = Git.open(dir)
55
+ puts "Open #{name}"
56
+ git_open
57
+ rescue Git::GitExecuteError
58
+ puts "Init #{name}"
59
+ Git.init(dir.to_s).tap do |git_init|
60
+ git_init.commit('Initial commit', allow_empty: true)
61
+ end
62
+ end
63
+ else
64
+ puts "Clone #{name}"
65
+ puts " #{repo}"
66
+ puts " #{dir}"
67
+
68
+ dir.mkpath
69
+ Git.clone(repo, dir)
70
+ end
71
+
72
+ # Add any existing files.
73
+ begin
74
+ git.add(all: true)
75
+ git.commit_all('Commit existing files')
76
+ rescue Git::GitExecuteError
77
+ # Nothing to do, continue with the rest of the sync.
78
+ end
79
+
80
+ # Ensure that the remote exists.
81
+ if git.remotes.empty?
82
+ puts 'Add remote....'
83
+ git.add_remote('origin', repo)
84
+ end
85
+
86
+ # TODO: should set upstream for regular commits
87
+
88
+ # Try synchronize the new commits with the remote origin.
89
+ begin
90
+ puts 'Rync with the remote'
91
+ git.pull('origin')
92
+ git.push('origin')
93
+ rescue Git::GitExecuteError
94
+ # Nothing to do, continue with the rest of the sync.
95
+ end
96
+ end
97
+ end
98
+
99
+ desc 'sync', 'run the repository synchronization once'
100
+ def sync(aspect = nil, year = nil)
101
+ aspect ||= 'prime'
102
+ year ||= Time.now.year
103
+
104
+ home = Pathname.new(Dir.home)
105
+ joined_dir = home.join('joined')
106
+ root = joined_dir.join(
107
+ 'git.andrewcant.ca',
108
+ 'andrew',
109
+ 'aspects',
110
+ aspect
111
+ )
112
+
113
+ gits =
114
+ [
115
+ root.join('dated', year.to_s),
116
+ root.join('dated', (year - 1).to_s),
117
+ root.join('gtd'),
118
+ root.join('wiki')
119
+ ].map { |x| x.glob('**/.git').map(&:dirname) }.flatten.map { |x| Git.open(x) }
120
+
121
+ gits.each do |git|
122
+ print '<'
123
+ # Add any existing files.
124
+ begin
125
+ git.add(all: true)
126
+ git.commit_all('auto commit')
127
+ rescue Git::GitExecuteError
128
+ # Nothing to do, continue with the rest of the sync.
129
+ end
130
+
131
+ print '.'
132
+
133
+ git.pull('origin')
134
+ git.push('origin')
135
+
136
+ print '>'
137
+ end
138
+ puts ''
139
+ end
140
+
141
+ desc 'syncer', 'keep the repositories synchroniztion'
142
+ def syncer
143
+ year ||= Time.now.year
144
+
145
+ home = Pathname.new(Dir.home)
146
+ joined_dir = home.join('joined')
147
+ aspects_root = joined_dir.join(
148
+ 'git.andrewcant.ca',
149
+ 'andrew',
150
+ 'aspects'
151
+ )
152
+
153
+ current_aspects = %w[prime sugar]
154
+ listen_pathnames =
155
+ current_aspects.map do |aspect|
156
+ aspect_dir = aspects_root.join(aspect)
157
+ [
158
+ aspect_dir.join('dated', year.to_s),
159
+ aspect_dir.join('dated', (year - 1).to_s),
160
+ aspect_dir.join('gtd'),
161
+ aspect_dir.join('wiki')
162
+ ]
163
+ end
164
+ # Flatten and include only existing directories.
165
+ listen_pathnames =
166
+ listen_pathnames.flatten.compact.select(&:directory?)
167
+
168
+ # Find the repositories within those directories.
169
+ listen_repository_pathnames =
170
+ listen_pathnames.map { |x| x.glob('**/.git').map(&:dirname) }.flatten
171
+
172
+ puts 'For these aspects:'
173
+ current_aspects.each { |x| puts " * #{x}" }
174
+ puts 'Watch the following repositories:'
175
+ listen_repository_pathnames.each { |x| puts " * #{x}" }
176
+
177
+ listener =
178
+ Listen.to(
179
+ *listen_repository_pathnames.map(&:to_s),
180
+ ignore: /#{File::SEPARATOR}\.git#{File::SEPARATOR}/o,
181
+ latency: 5,
182
+ wait_for_delay: 5
183
+ ) do |modified, added, removed|
184
+ changed_files = [modified, added, removed].flatten.compact.uniq
185
+ changed_files
186
+ .map { |file| listen_repository_pathnames.map(&:to_s).find { |repo| file.start_with?(repo) } }
187
+ .compact
188
+ .uniq
189
+ .map { |x| Git.open(x) }
190
+ .select { |x| x.status.added.any? || x.status.changed.any? || x.status.deleted.any? || x.status.untracked.any? } # rubocop:disable Layout/LineLength
191
+ .each do |git|
192
+ puts "Sync #{git.dir}"
193
+
194
+ # Add any existing files.
195
+ begin
196
+ git.add(all: true)
197
+ git.commit_all('auto commit')
198
+ rescue Git::GitExecuteError
199
+ # Nothing to do, continue with the rest of the sync.
200
+ end
201
+
202
+ # Sync the commits
203
+ # TODO: needs conflict handling. Review what is done in gitdocs.
204
+ begin
205
+ git.pull('origin')
206
+ git.push('origin')
207
+ rescue => e # rubocop:disable Style/RescueStandardError
208
+ puts "Error sycing #{git.dir}: #{e}"
209
+ end
210
+ end
211
+ end
212
+
213
+ listener.start
214
+ puts 'Start listening...'
215
+ begin
216
+ sleep
217
+ rescue Interrupt
218
+ puts 'Exit'
219
+ exit
220
+ rescue SignalException => e
221
+ puts "Exit on #{e}"
222
+ exit
223
+ end
224
+ end
225
+
226
+ desc 'show [PATH]', 'show the status of aspects and other repositories'
227
+ def show(aspect = nil, year = nil)
228
+ aspect ||= 'prime'
229
+ year ||= Time.now.year
230
+
231
+ home = Pathname.new(Dir.home)
232
+ joined_dir = home.join('joined')
233
+ aspect_dir = joined_dir.join(
234
+ 'git.andrewcant.ca',
235
+ 'andrew',
236
+ 'aspects',
237
+ aspect
238
+ )
239
+
240
+ gits =
241
+ [
242
+ aspect_dir.join('dated', year.to_s),
243
+ aspect_dir.join('dated', (year - 1).to_s),
244
+ aspect_dir.join('gtd'),
245
+ aspect_dir.join('wiki'),
246
+ aspect_dir.join('contacts'),
247
+ aspect_dir.join('password-store')
248
+ ].map { |x| x.glob('**/.git').map(&:dirname) }.flatten.map { |x| Git.open(x) }
249
+
250
+ output =
251
+ gits.map do |git|
252
+ additions = git.status.added.count + git.status.changed.count
253
+ OpenStruct.new(
254
+ path: Pathname.new(git.dir.to_s).relative_path_from(joined_dir).to_s,
255
+ status: "+#{additions} -#{git.status.deleted.count} *#{git.status.untracked.count}"
256
+ )
257
+ end
258
+
259
+ tp.set(:max_width, 256)
260
+ tp(output, :path, :status)
261
+ end
262
+ end
263
+ end
264
+
265
+ # rubocop:enable Metrics/ClassLength, Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity, Metrics/BlockLength
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Conjoiner
4
+ VERSION = '0.1.0'
5
+ end
data/lib/conjoiner.rb ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'conjoiner/version'
4
+ require 'conjoiner/cli'
5
+
6
+ module Conjoiner
7
+ class Error < StandardError; end
8
+ # Your code goes here...
9
+ #
10
+ end