gistore 1.0.0.rc4

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.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG +30 -0
  3. data/COPYING +340 -0
  4. data/README.md +98 -0
  5. data/exe/gistore +15 -0
  6. data/lib/gistore.rb +20 -0
  7. data/lib/gistore/cmd/add.rb +15 -0
  8. data/lib/gistore/cmd/checkout.rb +49 -0
  9. data/lib/gistore/cmd/commit.rb +171 -0
  10. data/lib/gistore/cmd/config.rb +23 -0
  11. data/lib/gistore/cmd/export-to-backups.rb +79 -0
  12. data/lib/gistore/cmd/gc.rb +15 -0
  13. data/lib/gistore/cmd/git-version.rb +14 -0
  14. data/lib/gistore/cmd/init.rb +36 -0
  15. data/lib/gistore/cmd/restore-from-backups.rb +91 -0
  16. data/lib/gistore/cmd/rm.rb +15 -0
  17. data/lib/gistore/cmd/safe-commands.rb +53 -0
  18. data/lib/gistore/cmd/status.rb +40 -0
  19. data/lib/gistore/cmd/task.rb +85 -0
  20. data/lib/gistore/cmd/version.rb +27 -0
  21. data/lib/gistore/config.rb +13 -0
  22. data/lib/gistore/config/gistore.yml +1 -0
  23. data/lib/gistore/error.rb +6 -0
  24. data/lib/gistore/repo.rb +683 -0
  25. data/lib/gistore/runner.rb +43 -0
  26. data/lib/gistore/templates/description +1 -0
  27. data/lib/gistore/templates/hooks/applypatch-msg.sample +15 -0
  28. data/lib/gistore/templates/hooks/commit-msg.sample +24 -0
  29. data/lib/gistore/templates/hooks/post-update.sample +8 -0
  30. data/lib/gistore/templates/hooks/pre-applypatch.sample +14 -0
  31. data/lib/gistore/templates/hooks/pre-commit.sample +49 -0
  32. data/lib/gistore/templates/hooks/pre-push.sample +54 -0
  33. data/lib/gistore/templates/hooks/pre-rebase.sample +169 -0
  34. data/lib/gistore/templates/hooks/prepare-commit-msg.sample +36 -0
  35. data/lib/gistore/templates/hooks/update.sample +128 -0
  36. data/lib/gistore/templates/info/exclude +6 -0
  37. data/lib/gistore/utils.rb +382 -0
  38. data/lib/gistore/version.rb +4 -0
  39. data/t/Makefile +80 -0
  40. data/t/README +745 -0
  41. data/t/aggregate-results.sh +46 -0
  42. data/t/lib-worktree.sh +76 -0
  43. data/t/t0000-init.sh +75 -0
  44. data/t/t0010-config.sh +75 -0
  45. data/t/t0020-version.sh +32 -0
  46. data/t/t1000-add-remove.sh +89 -0
  47. data/t/t1010-status.sh +87 -0
  48. data/t/t1020-commit.sh +134 -0
  49. data/t/t1030-commit-and-rotate.sh +266 -0
  50. data/t/t2000-task-and-commit-all.sh +132 -0
  51. data/t/t3000-checkout.sh +115 -0
  52. data/t/t3010-export-and-restore.sh +141 -0
  53. data/t/test-binary-1.png +0 -0
  54. data/t/test-binary-2.png +0 -0
  55. data/t/test-lib-functions.sh +722 -0
  56. data/t/test-lib.sh +684 -0
  57. metadata +161 -0
@@ -0,0 +1,91 @@
1
+ module Gistore
2
+ class Runner
3
+ desc "restore-from-backups", "Export to a series of full/increment backups"
4
+ option :from, :required => true, :desc => "path of backup packs", :banner => "<from>"
5
+ option :to, :required => true, :desc => "path of repo.git to restore to", :banner => "<repo.git>"
6
+ def restore_from_backups
7
+ parse_common_options
8
+ from = options[:from]
9
+ repo_name = options[:to]
10
+ backups = []
11
+ if not File.exist? from
12
+ raise "Path \"#{from}\" does not exist."
13
+ elsif File.directory? from
14
+ backups = Dir.glob("#{from}/*.pack")
15
+ elsif from.end_with? ".pack"
16
+ backups << from
17
+ end
18
+
19
+ if not backups or backups.empty?
20
+ raise "Can not find valid pack file(s) from \"#{from}\""
21
+ else
22
+ backups = backups.map {|p| File.expand_path(p.strip)}
23
+ end
24
+
25
+ if not File.exist? repo_name
26
+ Repo.init repo_name
27
+ elsif not Gistore.is_git_repo? repo_name
28
+ raise "Path \"#{repo_name}\" is not a valid repo, create one using \"gistore init\""
29
+ end
30
+
31
+ self.gistore = Repo.new(repo_name)
32
+ output = ""
33
+ backups.each do |pack|
34
+ begin
35
+ gistore.shellpipe(git_cmd, "unpack-objects", "-q",
36
+ :work_tree => gistore.repo_path,
37
+ :check_return => true
38
+ ) do |stdin, stdout, stderr|
39
+ File.open(pack, "r") do |io|
40
+ stdin.write io.read
41
+ end
42
+ stdin.close
43
+ output << stdout.read
44
+ output << "\n" unless output.empty?
45
+ output << stderr.read
46
+ end
47
+ rescue
48
+ Tty.warning "failed to unpack #{pack}"
49
+ end
50
+ end
51
+
52
+ danglings = []
53
+ cmds = [git_cmd, "fsck"]
54
+ cmds << "--dangling" if Gistore.git_version_compare('1.7.10') >= 0
55
+ gistore.shellout(*cmds) do |stdout|
56
+ stdout.readlines.each do |line|
57
+ if line =~ /^dangling commit (.*)/
58
+ danglings << $1.strip
59
+ end
60
+ end
61
+ end
62
+
63
+ unless danglings.empty?
64
+ begin
65
+ gistore.shellout(git_cmd, "rev-parse", "master", :check_return => true)
66
+ rescue
67
+ if danglings.size == 1
68
+ gistore.shellout(git_cmd, "update-ref", "refs/heads/master", danglings[0])
69
+ else
70
+ show_dangling_commits danglings
71
+ end
72
+ else
73
+ show_dangling_commits danglings
74
+ end
75
+ end
76
+ rescue Exception => e
77
+ Tty.error "Failed to restore-from-backups.\n#{output}"
78
+ Tty.die "#{e.message}"
79
+ end
80
+
81
+ private
82
+
83
+ def show_dangling_commits(danglings)
84
+ puts "Found dangling commits after restore backups:"
85
+ puts "\n"
86
+ puts Tty.show_columns danglings
87
+ puts "\n"
88
+ puts "You may like to update master branch with it(them) by hands."
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,15 @@
1
+ module Gistore
2
+ class Runner
3
+ desc "rm <path> ...", "Remove entry from backup list"
4
+ def rm(*args)
5
+ parse_common_options_and_repo
6
+ raise "nothing to remove." if args.empty?
7
+ args.each do |entry|
8
+ gistore.remove_entry entry
9
+ end
10
+ gistore.save_gistore_backups
11
+ rescue Exception => e
12
+ Tty.die "#{e.message}"
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,53 @@
1
+ module Gistore
2
+ SAFE_GIT_COMMANDS = %w(
3
+ annotate blame branch cat-file check-ignore
4
+ count-objects describe diff fsck grep
5
+ log lost-found ls-files ls-tree name-rev
6
+ prune reflog rev-list rev-parse shortlog
7
+ show show-ref status tag whatchanged)
8
+
9
+ class Runner
10
+ desc "repo <repo> git-command ...", "Delegate to safe git commands (log, ls-tree, ...)"
11
+ option :without_grafts, :type => :boolean, :desc => "not check info/grafts"
12
+ option :without_work_tree, :type => :boolean, :desc => "not change work tree"
13
+ option :without_locale, :type => :boolean, :desc => "use locale C"
14
+ def repo (name, cmd=nil, *args, &block)
15
+ parse_common_options
16
+ if options[:repo]
17
+ args ||= []
18
+ args.unshift cmd if cmd
19
+ name, cmd = options[:repo], name
20
+ end
21
+ cmd = args.shift if cmd == "git"
22
+ if Gistore::SAFE_GIT_COMMANDS.include? cmd
23
+ opts = options.dup
24
+ opts.delete("repo")
25
+ args << opts
26
+ gistore = Repo.new(name || ".")
27
+ gistore.safe_system(git_cmd, cmd, *args)
28
+ elsif self.respond_to? cmd
29
+ # Because command may have specific options mixed in args,
30
+ # can not move options from args easily. So we not call
31
+ # invoke here, but call Gistore::Runner.start.
32
+ Gistore::Runner.start([cmd, "--repo", name, *args])
33
+ else
34
+ raise "Command \"#{cmd}\" is not allowed.\n"
35
+ end
36
+ rescue Exception => e
37
+ Tty.die "#{e.message}"
38
+ end
39
+
40
+ desc "log args...", "Show gistore backup logs (delegater for git log)"
41
+ option :without_grafts, :type => :boolean, :desc => "not check info/grafts"
42
+ option :without_work_tree, :type => :boolean, :desc => "not change work tree"
43
+ option :without_locale, :type => :boolean, :desc => "use locale C"
44
+ def log(*args)
45
+ if options[:repo]
46
+ repo('log', *args)
47
+ else
48
+ name = args.shift || "."
49
+ repo(name, 'log', *args)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,40 @@
1
+ module Gistore
2
+ class Runner
3
+ map "st" => :status
4
+ desc "status", "Show backup entries list and run git status"
5
+ option :config, :type => :boolean, :desc => "Show gistore configurations"
6
+ option :backup, :type => :boolean,
7
+ :aliases => [:entries, :entry, :backups],
8
+ :desc => "Show backup list"
9
+ option :git, :type => :boolean, :desc => "Show git status"
10
+ def status(*args)
11
+ parse_common_options_and_repo
12
+ all = (not options.include? "config" and
13
+ not options.include? "backup" and
14
+ not options.include? "git")
15
+
16
+ if all or options[:config]
17
+ puts "Task name : #{gistore.task_name || "-"}"
18
+ puts "Gistore : #{gistore.repo_path}"
19
+ puts "Configurations:"
20
+ puts Tty.show_columns gistore.gistore_config.to_a.map {|h| "#{h[0]}: #{h[1].inspect}"}
21
+ puts
22
+ end
23
+
24
+ if all
25
+ puts "Backup entries:"
26
+ puts Tty.show_columns gistore.gistore_backups
27
+ puts
28
+ elsif options[:backup]
29
+ puts gistore.gistore_backups.join("\n")
30
+ end
31
+
32
+ if all or options[:git]
33
+ puts "#{'-' * 30} Git status #{'-' * 30}" if all
34
+ gistore.safe_system(git_cmd, "status", *args)
35
+ end
36
+ rescue Exception => e
37
+ Tty.die "#{e.message}"
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,85 @@
1
+ module Gistore
2
+ class Task < Thor; end
3
+
4
+ class Runner
5
+ desc "task SUBCOMMAND ...ARGS", "manage set of tracked repositories"
6
+ subcommand "task", Gistore::Task
7
+ end
8
+
9
+ class Task < Thor
10
+ # Use command name "gistore" in help instead of "gistore.rb"
11
+ def self.basename; "gistore"; end
12
+
13
+ # Show in help screen
14
+ package_name "Gistore"
15
+
16
+ desc "add <task> [<repo>]", "Register repo as a task"
17
+ option :system, :type => :boolean
18
+ def add(task, path=nil)
19
+ parse_common_options
20
+ path ||= "."
21
+ cmds = [git_cmd, "config"]
22
+ unless ENV["GISTORE_TEST_GIT_CONFIG"]
23
+ ENV.delete "GIT_CONFIG"
24
+ if options[:system]
25
+ cmds << "--system"
26
+ else
27
+ cmds << "--global"
28
+ end
29
+ end
30
+ cmds << "gistore.task.#{task}"
31
+ cmds << File.expand_path(path)
32
+ system(*cmds)
33
+ rescue Exception => e
34
+ Tty.die "#{e.message}"
35
+ end
36
+
37
+ desc "rm <task>", "Remove register of task"
38
+ option :system, :type => :boolean
39
+ def rm(task)
40
+ parse_common_options
41
+ cmds = [git_cmd, "config", "--unset"]
42
+ unless ENV["GISTORE_TEST_GIT_CONFIG"]
43
+ ENV.delete "GIT_CONFIG"
44
+ if options[:system]
45
+ cmds << "--system"
46
+ else
47
+ cmds << "--global"
48
+ end
49
+ end
50
+ cmds << "gistore.task.#{task}"
51
+ Kernel::system(*cmds)
52
+ rescue Exception => e
53
+ Tty.die "#{e.message}"
54
+ end
55
+
56
+ desc "list", "Display task list"
57
+ option :system, :type => :boolean
58
+ option :global, :type => :boolean
59
+ def list(name=nil)
60
+ parse_common_options
61
+ if name
62
+ invoke "gistore:runner:status", [], :repo => name
63
+ else
64
+ puts "System level Tasks"
65
+ tasks = Gistore::get_gistore_tasks(:system => true)
66
+ puts Tty.show_columns tasks.to_a.map {|h| "#{h[0]} => #{h[1]}"}
67
+ puts
68
+ puts "User level Tasks"
69
+ tasks = Gistore::get_gistore_tasks(:global => true)
70
+ puts Tty.show_columns tasks.to_a.map {|h| "#{h[0]} => #{h[1]}"}
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ def parse_common_options
77
+ if options[:verbose]
78
+ Tty.options[:verbose] = true
79
+ elsif options[:quiet]
80
+ Tty.options[:quiet] = true
81
+ end
82
+ end
83
+
84
+ end
85
+ end
@@ -0,0 +1,27 @@
1
+ require 'gistore/version'
2
+
3
+ module Gistore
4
+ class Runner
5
+ map ["--version", "-v"] => :version
6
+ desc "version", "Show Gistore version and/or repository format version", :hide => true
7
+ def version(*args)
8
+ parse_common_options
9
+ v = Gistore::VERSION
10
+ Dir.chdir(GISTORE_REPOSITORY) do
11
+ if Gistore.is_git_repo? ".git"
12
+ Gistore.shellout(git_cmd, "describe", "--always", "--dirty") do |stdout|
13
+ v = stdout.read.strip
14
+ v.sub!(/^v/, '')
15
+ v << " (#{Gistore::VERSION})" if v != Gistore::VERSION
16
+ end
17
+ end
18
+ end
19
+ puts "Gistore version #{v}"
20
+
21
+ gistore = Repo.new(options[:repo]) rescue nil if options[:repo]
22
+ if gistore
23
+ puts "Repository format #{gistore.repo_version}"
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,13 @@
1
+ module Gistore
2
+
3
+ DEFAULT_GISTORE_CONFIG = {"backups" => [],
4
+ "plan" => nil,
5
+ "increment_backup_number" => 30,
6
+ "full_backup_number" => 12,
7
+ "user_name" => nil,
8
+ "user_email" => nil
9
+ }
10
+ GISTORE_REPOSITORY_URL = "git://github.com/jiangxin/gistore"
11
+ GISTORE_REPOSITORY = File.dirname(File.dirname(File.dirname(__FILE__)))
12
+
13
+ end
@@ -0,0 +1 @@
1
+ ---
@@ -0,0 +1,6 @@
1
+ module Gistore
2
+ class CommandReturnError < StandardError; end
3
+ class CommandExceptionError < StandardError; end
4
+ class InvalidRepoError < StandardError; end
5
+ class CommandExit < StandardError; end
6
+ end
@@ -0,0 +1,683 @@
1
+ require 'yaml'
2
+ require 'fileutils'
3
+ require 'gistore/utils'
4
+ require 'gistore/error'
5
+ require 'gistore/config'
6
+
7
+ module Gistore
8
+
9
+ class Repo
10
+ attr_reader :task_name, :repo_path, :gistore_config, :gistore_backups
11
+
12
+ class <<self
13
+ def init(name, options = {})
14
+ if File.directory? name and Dir.entries(name).size != 2
15
+ raise "Non-empty directory '#{name}' is already exist."
16
+ else
17
+ FileUtils.mkdir_p name
18
+ end
19
+
20
+ # git initial --bare #{name}
21
+ ENV['GIT_TEMPLATE_DIR'] = File.join(File.dirname(__FILE__), 'templates')
22
+ # git-init can not take path as argument for git < v1.6.5
23
+ Dir.chdir(name) do
24
+ Gistore::shellout git_cmd, 'init', '--bare', :without_work_tree => true
25
+ end
26
+
27
+ gistore = Repo.new(name)
28
+
29
+ # Save repo version to "info/VERSION"
30
+ gistore.repo_version = Gistore::REPO_VERSION
31
+
32
+ # Set git config
33
+ gistore.git_config('--plan', options[:plan] || 'normal')
34
+
35
+ # Set gistore config
36
+ gistore.update_gistore_config("increment_backup_number" => 30,
37
+ "full_backup_number" => 12)
38
+ gistore.save_gistore_config
39
+ end
40
+ end
41
+
42
+ def initialize(name)
43
+ # Setup taskname
44
+ gistore_tasks = Gistore::get_gistore_tasks
45
+ if File.exist? name and
46
+ File.exist? name and
47
+ File.directory? name
48
+ @repo_path = realpath(name)
49
+ gistore_tasks.each do |t, p|
50
+ if realpath(p) == @repo_path
51
+ @task_name = t
52
+ break
53
+ end
54
+ end
55
+ elsif gistore_tasks.include? name and
56
+ File.exist? gistore_tasks[name] and
57
+ File.directory? gistore_tasks[name]
58
+ @repo_path = realpath gistore_tasks[name]
59
+ @task_name = name
60
+ else
61
+ raise InvalidRepoError.new("Can not find repo at \"#{name}\"")
62
+ end
63
+
64
+ raise InvalidRepoError.new("Not a valid git repo: #{@repo_path}") unless is_git_repo?
65
+
66
+ @gistore_config_file = "#{@repo_path}/info/gistore_config.yml"
67
+ @gistore_backups_file = "#{@repo_path}/info/gistore_backups.yml"
68
+ @gistore_exclude_file = "#{@repo_path}/info/exclude"
69
+ @gistore_version_file = "#{@repo_path}/info/VERSION"
70
+ @work_tree = "/"
71
+
72
+ parse_gistore_config
73
+
74
+ # Generate new info/exclude file
75
+ update_info
76
+ end
77
+
78
+ def normalize_entry(entry)
79
+ unless entry.start_with? '/'
80
+ Tty.error "entry not start with '/'"
81
+ return nil
82
+ end
83
+ # Note: not support UNC names
84
+ result = entry.gsub(/\/\/+/, '/')
85
+ if ['/'].include? result
86
+ Tty.error "root entry ignored!"
87
+ return nil
88
+ end
89
+ result
90
+ end
91
+
92
+ def validate_entry(entry)
93
+ return false unless entry
94
+ entry_path = realpath(entry)
95
+ if repo_path == entry_path or repo_path =~ /^#{entry_path}\//
96
+ Tty.warning "gistore repo is a subdir of entry: #{entry_path}"
97
+ return false
98
+ elsif entry_path =~ /^#{repo_path}\//
99
+ Tty.warning "entry is a subdir of gistore repo: #{repo_path}"
100
+ return false
101
+ else
102
+ return true
103
+ end
104
+ end
105
+
106
+ def add_entry(entry)
107
+ if entry
108
+ entry = File.expand_path(entry)
109
+ entry = normalize_entry(entry)
110
+ end
111
+ if not entry
112
+ Tty.warning "entry is nil"
113
+ elsif not validate_entry(entry)
114
+ Tty.warning "entry (#{entry}) is not valid."
115
+ elsif @gistore_backups.include? entry
116
+ Tty.warning "entry (#{entry}) is already added."
117
+ else
118
+ Tty.info "add entry: #{entry}."
119
+ @gistore_backups << entry
120
+ entry
121
+ end
122
+ end
123
+
124
+ def remove_entry(entry)
125
+ if not entry
126
+ Tty.warning "entry is nil"
127
+ else
128
+ unless @gistore_backups.include? entry
129
+ entry = File.expand_path(entry)
130
+ entry = normalize_entry(entry)
131
+ end
132
+ if @gistore_backups.delete entry
133
+ Tty.info "remove entry: #{entry}."
134
+ else
135
+ Tty.warning "entry (#{entry}) not in backup list, nothing removed."
136
+ end
137
+ end
138
+ end
139
+
140
+ def save_gistore_backups
141
+ backups = []
142
+ @gistore_backups.sort.each do |entry|
143
+ next if not validate_entry(entry)
144
+ if backups[-1] and entry.start_with? backups[-1]
145
+ if (entry.end_with? '/' or
146
+ backups[-1].size == entry.size or
147
+ entry.charat(backups[-1].size) == '/')
148
+ Tty.warning "remove \"#{entry}\", for it has been added as \"#{backups[-1]}\""
149
+ next
150
+ end
151
+ end
152
+ backups << entry
153
+ end
154
+ @gistore_backups = backups
155
+ f = File.new("#{@gistore_backups_file}.lock", 'w')
156
+ f.write(@gistore_backups.to_yaml)
157
+ f.close
158
+ File.rename("#{@gistore_backups_file}.lock", @gistore_backups_file)
159
+ end
160
+
161
+ def save_gistore_config
162
+ File.open("#{@gistore_config_file}.lock", 'w') do |io|
163
+ io.write(@gistore_config.to_yaml)
164
+ end
165
+ File.rename("#{@gistore_config_file}.lock", @gistore_config_file)
166
+ end
167
+
168
+ def update_gistore_config(options={})
169
+ options.each do |k, v|
170
+ @gistore_config[k.to_s] = v
171
+ end
172
+ end
173
+
174
+ def git_config(*args)
175
+ if Hash === args.last
176
+ options = args.pop.dup
177
+ else
178
+ options = {}
179
+ end
180
+ if args.size == 2 and (args[0] == '--plan' or args[0] == "plan")
181
+ git_config('core.quotepath', false)
182
+ git_config('core.autocrlf', false)
183
+ git_config('core.logAllRefUpdates', true)
184
+ git_config('core.sharedRepository', 'group')
185
+ git_config('core.bigFileThreshold', '2m')
186
+
187
+ case args[1]
188
+ when /no[-_]?gc/
189
+ git_config('gc.auto', 0)
190
+ git_config('core.compression', 0)
191
+ git_config('core.loosecompression', 0)
192
+ update_gistore_config("plan" => "no-gc")
193
+ when /no[-_]?compress/
194
+ git_config('--unset', 'gc.auto')
195
+ git_config('core.compression', 0)
196
+ git_config('core.loosecompression', 0)
197
+ update_gistore_config("plan" => "no-compress")
198
+ else
199
+ git_config('--unset', 'gc.auto')
200
+ git_config('--unset', 'core.compression')
201
+ git_config('--unset', 'core.loosecompression')
202
+ update_gistore_config("plan" => "normal")
203
+ end
204
+ save_gistore_config
205
+ else
206
+ k, v = args
207
+ if args.size == 0
208
+ puts Tty.show_columns gistore_config.to_a.map {|h| "#{h[0]} : #{h[1].inspect}"}
209
+ elsif args.size <= 2 and gistore_config.include? k
210
+ if args.size == 1
211
+ puts gistore_config[k]
212
+ elsif args.size == 2
213
+ update_gistore_config(k => v)
214
+ save_gistore_config
215
+ end
216
+ else
217
+ # Unset non-exist variable return error 5.
218
+ system(git_cmd, 'config', *args)
219
+ end
220
+ end
221
+ end
222
+
223
+ def generate_git_exclude
224
+ return if uptodate? @gistore_exclude_file, @gistore_backups_file
225
+ generate_git_exclude!
226
+ end
227
+
228
+ def generate_git_exclude!
229
+ backups = get_backups
230
+ hierarchies = []
231
+ excludes = []
232
+
233
+ backups.each do |entry|
234
+ excludes << entry
235
+ entries = entry.split(/\/+/)[1..-1].inject([]){|sum, e| sum << [sum[-1], e].join('/')}
236
+ entries.each do |e|
237
+ unless hierarchies.include? e
238
+ hierarchies << e
239
+ end
240
+ end
241
+ end
242
+ File.open(@gistore_exclude_file, "w") do |f|
243
+ f.puts "*"
244
+ # No trailing "/", because entry maybe a symlink
245
+ hierarchies.each do |entry|
246
+ f.puts "!#{entry}"
247
+ end
248
+ # Two trailing "**" will backup all files include files under subdir.
249
+ excludes.each do |entry|
250
+ if Gistore.git_version_compare('1.8.2') >= 0
251
+ f.puts "!#{entry}/**"
252
+ else
253
+ f.puts "!#{entry}/*"
254
+ end
255
+ end
256
+ end
257
+ end
258
+
259
+ def get_backups
260
+ backups = []
261
+ @gistore_backups.each do |entry|
262
+ entry = normalize_entry(entry)
263
+ next unless entry
264
+ next unless validate_entry(entry)
265
+ backups << entry
266
+ if File.exist?(entry)
267
+ entry_real = realpath(entry)
268
+ backups << entry_real if entry != entry_real
269
+ end
270
+ end
271
+ backups
272
+ end
273
+
274
+ def get_last_backups
275
+ gistore_config["backups"]
276
+ end
277
+
278
+ def update_info
279
+ # Because real directories pointed by entries may change,
280
+ # always generate info/exclude,
281
+ generate_git_exclude!
282
+ end
283
+
284
+ def setup_environment(options={})
285
+ if options[:without_work_tree]
286
+ ENV.delete 'GIT_WORK_TREE'
287
+ else
288
+ ENV['GIT_WORK_TREE'] = "."
289
+ end
290
+ if options[:without_grafts]
291
+ ENV['GIT_GRAFT_FILE'] = "/dev/null"
292
+ else
293
+ ENV.delete 'GIT_GRAFT_FILE'
294
+ end
295
+ if options[:without_locale]
296
+ ENV['LC_ALL'] = 'C'
297
+ else
298
+ ENV.delete 'LC_ALL'
299
+ end
300
+ unless options[:with_git_config]
301
+ ENV.delete 'GIT_CONFIG'
302
+ end
303
+ unless options[:without_gitdir]
304
+ ENV['GIT_DIR'] = repo_path
305
+ end
306
+ ENV.delete 'HOME'
307
+ ENV.delete 'XDG_CONFIG_HOME'
308
+ ENV['GIT_CONFIG_NOSYSTEM'] = '1'
309
+ ENV['GIT_AUTHOR_NAME'] = get_login
310
+ ENV['GIT_AUTHOR_EMAIL'] = get_email
311
+ ENV['GIT_COMMITTER_NAME'] = get_login
312
+ ENV['GIT_COMMITTER_EMAIL'] = get_email
313
+ end
314
+
315
+ # block has only 1 arg: stdout
316
+ def shellout(*args, &block)
317
+ if Hash === args.last
318
+ options = args.last.dup
319
+ else
320
+ options = {}
321
+ end
322
+ setup_environment(options)
323
+ if options[:without_work_tree]
324
+ Gistore::shellout(*args, &block)
325
+ else
326
+ Dir.chdir(options[:work_tree] || @work_tree) do
327
+ Gistore::shellout(*args, &block)
328
+ end
329
+ end
330
+ end
331
+
332
+ # block has 3 args: stdin, stdout, stderr
333
+ def shellpipe(*args, &block)
334
+ if Hash === args.last
335
+ options = args.last.dup
336
+ else
337
+ options = {}
338
+ end
339
+ setup_environment(options)
340
+ if options[:without_work_tree]
341
+ Gistore::shellpipe(*args, &block)
342
+ else
343
+ Dir.chdir(options[:work_tree] || @work_tree) do
344
+ Gistore::shellpipe(*args, &block)
345
+ end
346
+ end
347
+ end
348
+
349
+ def system(*args, &block)
350
+ if Hash === args.last
351
+ options = args.pop.dup
352
+ else
353
+ options = {}
354
+ end
355
+ setup_environment(options)
356
+ if options[:without_work_tree]
357
+ Gistore::system(*args, &block)
358
+ else
359
+ Dir.chdir(options[:work_tree] || @work_tree) do
360
+ Gistore::system(*args, &block)
361
+ end
362
+ end
363
+ end
364
+
365
+ # Same like system but with exceptions
366
+ def safe_system(*args, &block)
367
+ if Hash === args.last
368
+ options = args.pop.dup
369
+ else
370
+ options = {}
371
+ end
372
+ setup_environment(options)
373
+ if options[:without_work_tree]
374
+ Gistore::safe_system(*args, &block)
375
+ else
376
+ Dir.chdir(options[:work_tree] || @work_tree) do
377
+ Gistore::safe_system(*args, &block)
378
+ end
379
+ end
380
+ end
381
+
382
+ def quiet_system(*args, &block)
383
+ if Hash === args.last
384
+ options = args.pop.dup
385
+ else
386
+ options = {}
387
+ end
388
+ setup_environment(options)
389
+ if options[:without_work_tree]
390
+ Gistore::quiet_system(*args, &block)
391
+ else
392
+ Dir.chdir(options[:work_tree] || @work_tree) do
393
+ Gistore::quiet_system(*args, &block)
394
+ end
395
+ end
396
+ end
397
+
398
+ def repo_version
399
+ File.open(@gistore_version_file, "r") do |io|
400
+ io.read.to_s.strip
401
+ end
402
+ end
403
+
404
+ def repo_version=(ver)
405
+ File.open(@gistore_version_file, "w") do |io|
406
+ io.puts ver
407
+ end
408
+ end
409
+
410
+ def remove_submodules
411
+ submodules = []
412
+ shellpipe(git_cmd, "submodule", "status",
413
+ :without_locale => true,
414
+ :check_return => false) do |ignore, stdout, stderr|
415
+ stdout.readlines.each do |line|
416
+ line.strip!
417
+ if line =~ /.\w{40} (\w*) (.*)?/
418
+ submodules << Regexp.last_match(1)
419
+ end
420
+ end
421
+ stderr.readlines.each do |line|
422
+ line.strip!
423
+ if line =~ /No submodule mapping found in .gitmodules for path '(.*)'/
424
+ submodules << Regexp.last_match(1)
425
+ end
426
+ end
427
+ end
428
+ if submodules.empty?
429
+ []
430
+ else
431
+ Tty.debug "Remove submodules: #{submodules.join(", ")}"
432
+ system(git_cmd, "rm", "-f", "--cached", "-q", *submodules)
433
+ submodules
434
+ end
435
+ end
436
+
437
+ # add submodule as normal directory
438
+ def add_submodule(submodule, status=[])
439
+ # add tmp file in submodule
440
+ tmpfile = File.join(submodule, '.gistore-submodule')
441
+ File.open(File.join(@work_tree, tmpfile), 'w') {}
442
+
443
+ # git add tmp file in submodule
444
+ shellout(git_cmd, "add", "-f", tmpfile)
445
+
446
+ # git add whole submodule dir (-f to bypass .gitignore in parent dir)
447
+ shellout(git_cmd, "add", "-f", submodule)
448
+
449
+ # git rm -f tmp file in submodule
450
+ shellout(git_cmd, "rm", "-f", tmpfile)
451
+
452
+ # Read status
453
+ shellout git_cmd, "status", "--porcelain", "--", submodule do |stdout|
454
+ stdout.readlines.each do |line|
455
+ line.strip!
456
+ status << line unless line.empty?
457
+ end
458
+ end
459
+ status
460
+ end
461
+
462
+ def backup_rotate
463
+ increment_backup_number = gistore_config["increment_backup_number"].to_i
464
+ full_backup_number = gistore_config["full_backup_number"].to_i
465
+ return if full_backup_number == 0 and increment_backup_number == 0
466
+ increment_backup_number = 30 if increment_backup_number < 1
467
+ full_backup_number = 6 if full_backup_number < 1
468
+
469
+ count = shellout(git_cmd, "rev-list", "master",
470
+ :without_grafts => true,
471
+ :check_return => false){|stdout| stdout.readlines.size}.to_i
472
+ if count <= increment_backup_number
473
+ Tty.debug "no backup rotate needed. #{count} <= #{increment_backup_number}"
474
+ return
475
+ else
476
+ Tty.debug "start to rotate branch, because #{count} > #{increment_backup_number}"
477
+ end
478
+
479
+ # list branches with prefix: gistore/
480
+ branches = []
481
+ cmds = [git_cmd, "branch"]
482
+ cmds += ["--list", "gistore/*"] if Gistore.git_version_compare('1.7.8') >= 0
483
+ shellout *cmds do |stdout|
484
+ stdout.readlines.each do |line|
485
+ line.strip!
486
+ branches << line if line =~ /^gistore\//
487
+ end
488
+ end
489
+ branches.sort!
490
+
491
+ # Remove unwanted branches
492
+ if branches.size >= full_backup_number and full_backup_number > 0
493
+ until branches.size <= full_backup_number
494
+ right = branches.pop
495
+ shellout git_cmd, "branch", "-D", right
496
+ Tty.debug "deleted unwanted branch - #{right}"
497
+ end
498
+ end
499
+
500
+ # Add new branch to branches
501
+ if branches.size < full_backup_number
502
+ if branches.empty?
503
+ branches << "gistore/1"
504
+ else
505
+ branches << branches[-1].succ
506
+ end
507
+ end
508
+
509
+ # Rotate branches
510
+ branches_dup = branches.dup
511
+ right = nil
512
+ until branches_dup.empty? do
513
+ left = branches_dup.pop
514
+ if left and right
515
+ # shellout git_cmd, "update-ref", "refs/heads/#{right}", "refs/heads/#{left}"
516
+ shellout git_cmd, "branch", "-f", right, left
517
+ Tty.debug "update branch #{right} (value from #{left})"
518
+ end
519
+ right = left
520
+ end
521
+
522
+ # Save master to gistore/1
523
+ old_branch = "master"
524
+ new_branch = branches[0] || "gistore/1"
525
+ shellout git_cmd, "branch", "-f", new_branch, old_branch
526
+ Tty.debug "update branch #{new_branch} (from master)"
527
+
528
+ # Run: git cat-file commit master | \
529
+ # sed '/^parent/ d' | \
530
+ # git hash-object -t commit -w --stdin
531
+ cobj = ""
532
+ shellout git_cmd, "cat-file", "commit", "master" do |stdout|
533
+ end_of_header = false
534
+ stdout.readlines.each do |line|
535
+ if line !~ /^parent [0-9a-f]{40}\s*$/ or end_of_header
536
+ cobj << line
537
+ end
538
+ if line == "\n" and not end_of_header
539
+ cobj << <<-EDIT_COMMIT_LOG
540
+ Full backup of #{task_name || File.basename(repo_path)}
541
+
542
+ #{Tty.show_columns gistore_backups}
543
+
544
+ ** Copy from this commit **
545
+
546
+ EDIT_COMMIT_LOG
547
+ end_of_header = true
548
+ end
549
+ end
550
+ end
551
+
552
+ object_id = shellpipe(git_cmd, "hash-object",
553
+ "-t", "commit",
554
+ "-w", "--stdin") do |stdin, stdout, stderr|
555
+ stdin.puts cobj
556
+ stdin.close
557
+ stdout.read
558
+ end.to_s.strip
559
+
560
+ raise "Bad object_id created by 'git hash-object'" if object_id !~ /^[0-9a-f]{40}$/
561
+ shellout git_cmd, "update-ref", "refs/heads/master", object_id
562
+ Tty.debug "update master with #{object_id}"
563
+
564
+ # create file .git/info/grafts.
565
+ # parent of object_id -> gistore/N^
566
+ # paretn of gistore/N last commit -> gistore/(N-1)^
567
+ grafts_file = File.join(repo_path, "info", "grafts")
568
+ grafts = [object_id]
569
+
570
+ branches.each do |branch|
571
+ shellout(git_cmd, "rev-list", "refs/heads/#{branch}",
572
+ :without_grafts => true) do |stdout|
573
+ lines = stdout.readlines
574
+ # lines[0] and object_id points to same tree.
575
+ new = lines.size > 1 ? lines[1] : lines[0]
576
+ old = lines[-1]
577
+ grafts << new.strip
578
+ grafts << old.strip
579
+ end
580
+ end
581
+ File.open(grafts_file, "w") do |io|
582
+ until grafts.empty?
583
+ left = grafts.shift
584
+ right = grafts.shift
585
+ if left and right
586
+ io.puts "#{left} #{right}"
587
+ end
588
+ end
589
+ end
590
+ end
591
+
592
+ def git_gc(*args)
593
+ if Hash === args.last
594
+ options = args.pop.dup
595
+ else
596
+ options = {}
597
+ end
598
+ gc_enabled = true
599
+ shellout git_cmd, "config", "gc.auto" do |stdout|
600
+ if stdout.read.strip == "0"
601
+ gc_enabled = false
602
+ end
603
+ end
604
+ if gc_enabled
605
+ if options[:force]
606
+ safe_system git_cmd, "reflog", "expire", "--expire=now", "--all", :without_work_tree => true
607
+ safe_system git_cmd, "prune", "--expire=now", :without_work_tree => true
608
+ args.delete "--auto" if args.include? "--auto"
609
+ args << {:without_work_tree => true}
610
+ safe_system git_cmd, "gc", *args
611
+ else
612
+ args.unshift "--auto" unless args.include? "--auto"
613
+ args << {:without_work_tree => true}
614
+ safe_system git_cmd, "gc", *args
615
+ end
616
+ else
617
+ Tty.warning "GC is disabled."
618
+ end
619
+ end
620
+
621
+ private
622
+ def is_git_repo?
623
+ Gistore.is_git_repo? repo_path
624
+ end
625
+
626
+ def load_default_config
627
+ @gistore_config = DEFAULT_GISTORE_CONFIG
628
+ gistore_default_config_file = File.join(File.dirname(__FILE__),
629
+ "config/gistore.yml")
630
+ @gistore_config.merge!(YAML::load_file(gistore_default_config_file) || {})
631
+ @gistore_backups = []
632
+ end
633
+
634
+ def parse_gistore_config
635
+ load_default_config
636
+ if File.exist? @gistore_config_file
637
+ @gistore_config.merge!(YAML::load_file(@gistore_config_file))
638
+ end
639
+ if File.exist? @gistore_backups_file
640
+ @gistore_backups = YAML::load_file(@gistore_backups_file)
641
+ end
642
+ end
643
+
644
+ def realpath(entry)
645
+ if File.exist? entry
646
+ if File.respond_to? :realpath
647
+ File.realpath(entry)
648
+ else
649
+ # ruby 1.8
650
+ require 'pathname'
651
+ Pathname.new(entry).realpath.to_s
652
+ end
653
+ else
654
+ File.expand_path(entry)
655
+ end
656
+ end
657
+
658
+ def uptodate?(file, *depends)
659
+ if File.exist?(file) and FileUtils::uptodate?(file, depends)
660
+ true
661
+ else
662
+ false
663
+ end
664
+ end
665
+
666
+ def get_login
667
+ username = gistore_config["username"]
668
+ username = `#{git_cmd} config user.name`.strip if username.nil? or username.empty?
669
+ if username.nil? or username.empty?
670
+ require 'etc'
671
+ username = Etc::getlogin
672
+ end
673
+ username
674
+ end
675
+
676
+ def get_email
677
+ useremail = gistore_config["useremail"]
678
+ useremail = `#{git_cmd} config user.email`.strip if useremail.nil? or useremail.empty?
679
+ useremail = "none" if useremail.nil? or useremail.empty?
680
+ useremail
681
+ end
682
+ end
683
+ end