cir 0.1

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,35 @@
1
+ # Licensed under the Apache License, Version 2.0 (the "License");
2
+ # you may not use this file except in compliance with the License.
3
+ # You may obtain a copy of the License at
4
+ #
5
+ # http://www.apache.org/licenses/LICENSE-2.0
6
+ #
7
+ # Unless required by applicable law or agreed to in writing, software
8
+ # distributed under the License is distributed on an "AS IS" BASIS,
9
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
+ # See the License for the specific language governing permissions and
11
+ # limitations under the License.
12
+ #
13
+
14
+ module Cir
15
+ module Cli
16
+ ##
17
+ # Register command
18
+ class RegisterCommand < CommandWithRepository
19
+
20
+ def opts
21
+ Trollop::Parser.new do
22
+ banner "Start tracking new file(s)."
23
+ opt :message, "Optional commit message that should be used when updating the changes in tracking git repository.", type: :string
24
+ end
25
+ end
26
+
27
+ def process
28
+ Trollop::die "Missing file list" if self.files.empty?
29
+
30
+ self.repository.register(self.files, {message: self.args.message})
31
+ end
32
+
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,34 @@
1
+ # Licensed under the Apache License, Version 2.0 (the "License");
2
+ # you may not use this file except in compliance with the License.
3
+ # You may obtain a copy of the License at
4
+ #
5
+ # http://www.apache.org/licenses/LICENSE-2.0
6
+ #
7
+ # Unless required by applicable law or agreed to in writing, software
8
+ # distributed under the License is distributed on an "AS IS" BASIS,
9
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
+ # See the License for the specific language governing permissions and
11
+ # limitations under the License.
12
+ #
13
+
14
+ module Cir
15
+ module Cli
16
+ ##
17
+ # Restore command
18
+ class RestoreCommand < CommandWithRepository
19
+
20
+ def opts
21
+ Trollop::Parser.new do
22
+ banner "Discard local changes and restore last known version of the file (~ git reset)"
23
+ end
24
+ end
25
+
26
+ def process
27
+ Trollop::die "Missing file list" if self.files.empty?
28
+
29
+ self.repository.restore(self.files)
30
+ end
31
+
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,43 @@
1
+ # Licensed under the Apache License, Version 2.0 (the "License");
2
+ # you may not use this file except in compliance with the License.
3
+ # You may obtain a copy of the License at
4
+ #
5
+ # http://www.apache.org/licenses/LICENSE-2.0
6
+ #
7
+ # Unless required by applicable law or agreed to in writing, software
8
+ # distributed under the License is distributed on an "AS IS" BASIS,
9
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
+ # See the License for the specific language governing permissions and
11
+ # limitations under the License.
12
+ #
13
+
14
+ module Cir
15
+ module Cli
16
+ ##
17
+ # Status command
18
+ class StatusCommand < CommandWithRepository
19
+
20
+ def opts
21
+ Trollop::Parser.new do
22
+ banner "Show status of registered files."
23
+ opt :show_diff, "Show diffs for changed files", :default => false
24
+ opt :all, "Display all files even those that haven't been changed", :default => false
25
+ end
26
+ end
27
+
28
+ def process
29
+ files = self.repository.status(self.files.empty? ? nil : self.files)
30
+
31
+ files.each do |file|
32
+ diff = file.diff
33
+ if diff.changed?
34
+ puts "File #{file.file_path} changed."
35
+ puts "#{diff.to_s}\n" if self.args[:show_diff]
36
+ elsif self.args[:all]
37
+ puts "File #{file.file_path} is the same."
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,35 @@
1
+ # Licensed under the Apache License, Version 2.0 (the "License");
2
+ # you may not use this file except in compliance with the License.
3
+ # You may obtain a copy of the License at
4
+ #
5
+ # http://www.apache.org/licenses/LICENSE-2.0
6
+ #
7
+ # Unless required by applicable law or agreed to in writing, software
8
+ # distributed under the License is distributed on an "AS IS" BASIS,
9
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
+ # See the License for the specific language governing permissions and
11
+ # limitations under the License.
12
+ #
13
+
14
+ module Cir
15
+ module Cli
16
+ ##
17
+ # Update command
18
+ class UpdateCommand < CommandWithRepository
19
+
20
+ def opts
21
+ Trollop::Parser.new do
22
+ banner "Note new local changes to the tracked file(s) in the repository (~ git commit)"
23
+ opt :message, "Optional commit message that should be used when updating the changes in tracking git repository.", type: :string
24
+ end
25
+ end
26
+
27
+ def process
28
+ Trollop::die "Missing file list" if self.files.empty?
29
+
30
+ self.repository.update(self.files, {message: self.args.message})
31
+ end
32
+
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,52 @@
1
+ # Licensed under the Apache License, Version 2.0 (the "License");
2
+ # you may not use this file except in compliance with the License.
3
+ # You may obtain a copy of the License at
4
+ #
5
+ # http://www.apache.org/licenses/LICENSE-2.0
6
+ #
7
+ # Unless required by applicable law or agreed to in writing, software
8
+ # distributed under the License is distributed on an "AS IS" BASIS,
9
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
+ # See the License for the specific language governing permissions and
11
+ # limitations under the License.
12
+ #
13
+ require 'diffy'
14
+
15
+ module Cir
16
+ ##
17
+ # Abstraction above chosen diff library so that we can switch it at runtime if/when needed
18
+ class DiffManager
19
+
20
+ def self.create(source, destination)
21
+ # Compare stored version in our internal repo and then the current version
22
+ diff = Diffy::Diff.new(source, destination, source: "files", diff: "-U 3")
23
+
24
+ # And finally return diff object with standardized interface
25
+ DiffManager.new(diff)
26
+ end
27
+
28
+ private
29
+
30
+ ##
31
+ # Persist generated diff inside the class
32
+ def initialize(diff)
33
+ @diff = diff
34
+ end
35
+
36
+ public
37
+
38
+ ##
39
+ # Return true if the files are different (e.g. diff is non-empty)
40
+ def changed?
41
+ return !@diff.to_s.empty?
42
+ end
43
+
44
+ ##
45
+ # Serialize the diff into string that can be printed to the console
46
+ def to_s
47
+ # We want nice colors by default
48
+ @diff.to_s(:color)
49
+ end
50
+
51
+ end # class DiffManager
52
+ end # module Cir
@@ -0,0 +1,28 @@
1
+ # Licensed under the Apache License, Version 2.0 (the "License");
2
+ # you may not use this file except in compliance with the License.
3
+ # You may obtain a copy of the License at
4
+ #
5
+ # http://www.apache.org/licenses/LICENSE-2.0
6
+ #
7
+ # Unless required by applicable law or agreed to in writing, software
8
+ # distributed under the License is distributed on an "AS IS" BASIS,
9
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
+ # See the License for the specific language governing permissions and
11
+ # limitations under the License.
12
+ #
13
+ module Cir
14
+ module Exception
15
+ ##
16
+ # Thrown in case that we're initializing already existing repository
17
+ class AlreadyRegistered < RuntimeError; end
18
+
19
+ ##
20
+ # Thrown in case that we're trying to access non existing repository
21
+ class RepositoryExists < RuntimeError; end
22
+
23
+ ##
24
+ # Thrown if we're trying to work with file that haven't been registered
25
+ class NotRegistered < RuntimeError; end
26
+ end
27
+ end
28
+
@@ -0,0 +1,96 @@
1
+ # Licensed under the Apache License, Version 2.0 (the "License");
2
+ # you may not use this file except in compliance with the License.
3
+ # You may obtain a copy of the License at
4
+ #
5
+ # http://www.apache.org/licenses/LICENSE-2.0
6
+ #
7
+ # Unless required by applicable law or agreed to in writing, software
8
+ # distributed under the License is distributed on an "AS IS" BASIS,
9
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
+ # See the License for the specific language governing permissions and
11
+ # limitations under the License.
12
+ #
13
+ require 'rugged'
14
+
15
+ module Cir
16
+ ##
17
+ # Class wrapping underlying Git library (rugged) and making simple certain operations
18
+ # that cir needs to do the most.
19
+ class GitRepository
20
+
21
+ ##
22
+ # Create new git repository
23
+ def self.create(rootPath)
24
+ raise Cir::Exception::RepositoryExists, "Path #{rootPath} already exists." if Dir.exists?(rootPath)
25
+
26
+ # Without remote we will create blank new repository
27
+ Rugged::Repository.init_at(rootPath)
28
+
29
+ # And return our own wrapper on top of the underlying Rugged object
30
+ Cir::GitRepository.new(rootPath)
31
+ end
32
+
33
+ ##
34
+ # Open given existing repository on disk. You might need to {#create} one if needed.
35
+ def initialize(rootPath)
36
+ @repo = Rugged::Repository.new(rootPath)
37
+ end
38
+
39
+ ##
40
+ # Adds given file to index, so that it's properly tracked for next commit. This file *must* contain
41
+ # local path relative to the root of working directory.
42
+ def add_file(file)
43
+ index = @repo.index
44
+ index.add path: file, oid: (Rugged::Blob.from_workdir @repo, file), mode: 0100644
45
+ index.write
46
+ end
47
+
48
+ ##
49
+ # Remove given file from the repository. The path must have the same characteristics as it have
50
+ # for #add_file method.
51
+ def remove_file(file)
52
+ index = @repo.index
53
+ index.remove file
54
+ end
55
+
56
+ ##
57
+ # Path to the root of the repository
58
+ def repository_root
59
+ File.expand_path(@repo.path + "/../")
60
+ end
61
+
62
+ ##
63
+ # Commit all staged changes to the git repository
64
+ def commit(message = nil)
65
+ # Write current index back to the repository
66
+ index = @repo.index
67
+ commit_tree = index.write_tree @repo
68
+
69
+ # Commit message
70
+ files = []
71
+ index.each {|i| files << File.basename(i[:path]) }
72
+ message = "Affected files: #{files.join(', ')}" if message.nil?
73
+
74
+ # User handling
75
+ user = ENV['USER']
76
+ user = "cir-out-commit" if user.nil?
77
+
78
+ # Commit author structure for git
79
+ commit_author = {
80
+ email: 'cir-auto-commit@nowhere.cz',
81
+ name: user,
82
+ time: Time.now
83
+ }
84
+
85
+ # And finally commit itself
86
+ Rugged::Commit.create @repo,
87
+ author: commit_author,
88
+ committer: commit_author,
89
+ message: message,
90
+ parents: @repo.empty? ? [] : [ @repo.head.target ].compact,
91
+ tree: commit_tree,
92
+ update_ref: 'HEAD'
93
+ end
94
+
95
+ end # class GitRepository
96
+ end # module Cir
@@ -0,0 +1,209 @@
1
+ # Licensed under the Apache License, Version 2.0 (the "License");
2
+ # you may not use this file except in compliance with the License.
3
+ # You may obtain a copy of the License at
4
+ #
5
+ # http://www.apache.org/licenses/LICENSE-2.0
6
+ #
7
+ # Unless required by applicable law or agreed to in writing, software
8
+ # distributed under the License is distributed on an "AS IS" BASIS,
9
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
+ # See the License for the specific language governing permissions and
11
+ # limitations under the License.
12
+ #
13
+ require 'yaml/store'
14
+
15
+ module Cir
16
+ ##
17
+ # Main database with tracked files and such.
18
+ class Repository
19
+ ##
20
+ # Outside of the git repository we also have separate database of all files that we're tracking with
21
+ # additional metadata stored in this yaml file.
22
+ FILE_LIST = 'cir.file_list.yml'
23
+
24
+ ##
25
+ # Create new repository backend (initialize git repo and the metadata database)
26
+ def self.create(rootPath)
27
+ git = Cir::GitRepository.create(rootPath)
28
+
29
+ # Create database
30
+ database = YAML::Store.new(rootPath + '/' + FILE_LIST)
31
+ database.transaction do
32
+ database[:version] = 1
33
+ database[:files] = {}
34
+ end
35
+
36
+ # Add it to git and finish
37
+ git.add_file FILE_LIST
38
+ git.commit
39
+ end
40
+
41
+ ##
42
+ # Load repository (must exists) from given path.
43
+ def initialize(rootPath)
44
+ # Database with files and their characteristics
45
+ @git = Cir::GitRepository.new(rootPath)
46
+ @database = YAML::Store.new(rootPath + '/' + FILE_LIST)
47
+ end
48
+
49
+ ##
50
+ # Register new file. Given path must be absolute.
51
+ #
52
+ # Options
53
+ # :message -> optional git message that should be used
54
+ def register(files, options = {})
55
+ expand(files) do |file|
56
+ # Register is one time operation, one can't re-register existing file
57
+ raise Cir::Exception::AlreadyRegistered, file if registered?(file)
58
+
59
+ # Import file to repository
60
+ import_file file
61
+
62
+ # Create new metadata for the tracked file
63
+ @database.transaction { @database[:files][file] = {} }
64
+
65
+ puts "Registering file: #{file}"
66
+ end
67
+
68
+ # And finally commit the transaction
69
+ @git.commit(options[:message])
70
+ end
71
+
72
+ ##
73
+ # Deregister file
74
+ #
75
+ # Options
76
+ # :message -> optional git message that should be used
77
+ def deregister(files, options = {})
78
+ @database.transaction do
79
+ expand(files) do |file|
80
+ stored = stored_file(file)
81
+
82
+ # Remove the file from git, our database and finally from git working directory
83
+ FileUtils.rm(stored.repository_location)
84
+ @git.remove_file(file[1..-1]) # Removing leading "/" to make the absolute path relative to the repository's root
85
+ @database[:files].delete(file)
86
+
87
+ puts "Deregistering file: #{file}"
88
+ end
89
+ end
90
+
91
+ # And finally commit the transaction
92
+ @git.commit(options[:message])
93
+ end
94
+
95
+
96
+ ##
97
+ # Returns true if given file is registered, otherwise false.
98
+ def registered?(file)
99
+ @database.transaction { return @database[:files][file] != nil }
100
+ end
101
+
102
+ ##
103
+ # Return status for all registered files
104
+ def status(requested_files = nil)
105
+ generate_file_list(requested_files)
106
+ end
107
+
108
+
109
+ ##
110
+ # Will update stored variant of existing files with their newer copy
111
+ #
112
+ # Options
113
+ # :message -> optional git message that should be used
114
+ def update(requested_files = nil, options = {})
115
+ generate_file_list(requested_files).each do |file|
116
+ if file.diff.changed?
117
+ import_file(file.file_path)
118
+ puts "Updating #{file.file_path}"
119
+ end
120
+ end
121
+
122
+ # Finally commit the transaction
123
+ @git.commit(options[:message])
124
+ end
125
+
126
+ ##
127
+ # Restore persistent variant of the files
128
+ def restore(requested_files = nil, force = false)
129
+ generate_file_list(requested_files).each do |file|
130
+ # If the destination file doesn't exist, we will simply copy it over
131
+ if not File.exists?(file.file_path)
132
+ FileUtils.cp(file.repository_location, file.file_path)
133
+ puts "Restoring #{file.file_path}"
134
+ next
135
+ end
136
+
137
+ # Skipping files that did not changed
138
+ next unless file.diff.changed?
139
+
140
+ # If we're run with force or in case of specific files, remove existing file and replace it
141
+ if force or not requested_files.nil?
142
+ FileUtils.remove_entry(file.file_path)
143
+ FileUtils.cp(file.repository_location, file.file_path)
144
+ puts "Restoring #{file.file_path}"
145
+ else
146
+ puts "Skipped mass change to #{key}."
147
+ end
148
+ end
149
+ end
150
+
151
+ private
152
+
153
+ ##
154
+ # Prepare file list for commands that accepts multiple files or none at all
155
+ def generate_file_list(requested_files)
156
+ files = []
157
+
158
+ @database.transaction do
159
+ if requested_files.nil?
160
+ # No file list, go over all files and detect if they changed
161
+ @database[:files].each { |file, value| files << stored_file(file) }
162
+ else
163
+ # User supplied set of files
164
+ expand(requested_files) { |file| files << stored_file(file) }
165
+ end
166
+ end
167
+
168
+ files
169
+ end
170
+
171
+ # Create stored file entity for given file (full path)
172
+ def stored_file(file)
173
+ raise Cir::Exception::NotRegistered, file unless @database[:files].include? file
174
+
175
+ return Cir::StoredFile.new(
176
+ file_path: file,
177
+ repository_location: File.expand_path(@git.repository_root + "/" + file)
178
+ )
179
+ end
180
+
181
+ ##
182
+ # Import given file to git repository and add it to index
183
+ def import_file(file)
184
+ target_file = File.expand_path(@git.repository_root + "/" + file)
185
+ target_dir = File.dirname(target_file)
186
+
187
+ if File.exists?(target_file)
188
+ FileUtils.rm_rf(target_file, secure: true)
189
+ else
190
+ FileUtils.mkdir_p(target_dir)
191
+ end
192
+
193
+ # And finally copy the file to repository
194
+ FileUtils.cp(file, target_file)
195
+
196
+ # And register it inside git and our metadata database
197
+ @git.add_file(file[1..-1]) # Removing leading "/" to make the absolute path relative to the repository's root
198
+ end
199
+
200
+ ##
201
+ # Loop over user specified file paths that will always expand the specified path
202
+ def expand(files)
203
+ files.each do |file|
204
+ expanded = File.expand_path(file)
205
+ yield expanded
206
+ end
207
+ end
208
+ end # end class Repository
209
+ end # end module Cir