conjoiner 0.1.0

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.
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