gistore 1.0.0.rc4

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