cir 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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