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.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.gitlab-ci.yml +27 -0
- data/.rubocop.yml +10 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +7 -0
- data/Gemfile +8 -0
- data/LICENSE +661 -0
- data/README.md +87 -0
- data/Rakefile +26 -0
- data/bin/console +9 -0
- data/bin/setup +9 -0
- data/ca.andrewsullivancant.conjoiner.plist +23 -0
- data/conjoiner.gemspec +47 -0
- data/conjoiner.service +9 -0
- data/cucumber.yml +1 -0
- data/exe/conjoiner +7 -0
- data/lib/conjoiner/cli.rb +265 -0
- data/lib/conjoiner/version.rb +5 -0
- data/lib/conjoiner.rb +10 -0
- metadata +263 -0
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
data/bin/setup
ADDED
@@ -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
data/cucumber.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
default: --publish-quiet
|
data/exe/conjoiner
ADDED
@@ -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
|