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
data/exe/gistore ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+
4
+ std_trap = trap("INT") { exit! 130 } # no backtrace thanks
5
+
6
+ require 'gistore/runner'
7
+ require 'gistore/utils'
8
+
9
+ abort "Please install git first" unless git_cmd
10
+ if Gistore.git_version_compare('1.6.0') < 0
11
+ abort "Git lower than 1.6.0 has not been tested. Please upgrade your git."
12
+ end
13
+
14
+ $gistore_runner = true
15
+ Gistore::Runner.start
data/lib/gistore.rb ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+
4
+ std_trap = trap("INT") { exit! 130 } # no backtrace thanks
5
+
6
+ require 'pathname'
7
+ LIBRARY_PATH = Pathname.new(__FILE__).realpath.dirname.to_s
8
+ $:.unshift(LIBRARY_PATH + '/gistore/vendor')
9
+ $:.unshift(LIBRARY_PATH)
10
+
11
+ require 'gistore/runner'
12
+ require 'gistore/utils'
13
+
14
+ abort "Please install git first" unless git_cmd
15
+ if Gistore.git_version_compare('1.6.0') < 0
16
+ abort "Git lower than 1.6.0 has not been tested. Please upgrade your git."
17
+ end
18
+
19
+ $gistore_runner = true
20
+ Gistore::Runner.start
@@ -0,0 +1,15 @@
1
+ module Gistore
2
+ class Runner
3
+ desc "add <path> ...", "Add path to backup list"
4
+ def add(*args)
5
+ parse_common_options_and_repo
6
+ raise "nothing to add." if args.empty?
7
+ args.each do |entry|
8
+ gistore.add_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,49 @@
1
+ module Gistore
2
+ class Runner
3
+ desc "checkout [--rev <rev>]",
4
+ "Checkout entries to <path>"
5
+ option :rev, :aliases => [:r], :desc => "Revision to checkout", :banner => "<rev>"
6
+ option :to, :required => true, :banner => "<path>", :desc => "a empty directory to save checkout items"
7
+ def checkout(*args)
8
+ parse_common_options_and_repo
9
+ work_tree = options[:to]
10
+ if File.exist? work_tree
11
+ if not File.directory? work_tree
12
+ Tty.die "\"#{work_tree}\" is not a valid directory."
13
+ elsif File.file? File.join(work_tree, ".git")
14
+ gitfile = File.open(File.join(work_tree, ".git")) {|io| io.read}.strip
15
+ if gitfile != "gitdir: #{gistore.repo_path}"
16
+ Tty.die "\"#{work_tree}\" not a checkout from #{gistore.repo_path}"
17
+ end
18
+ elsif Dir.entries(work_tree).size != 2
19
+ Tty.die "\"#{work_tree}\" is not a blank directory."
20
+ end
21
+ else
22
+ require 'fileutils'
23
+ FileUtils.mkdir_p work_tree
24
+ File.open(File.join(work_tree, '.git'), 'w') do |io|
25
+ io.puts "gitdir: #{gistore.repo_path}"
26
+ end
27
+ end
28
+ if git_version_compare('1.7.7.1') >= 0
29
+ args = args.empty? ? ["."]: args.dup
30
+ args << {:work_tree => work_tree}
31
+ args.shift if args.first == '--'
32
+ cmds = [git_cmd,
33
+ "checkout",
34
+ options[:rev] || 'HEAD',
35
+ "--",
36
+ *args]
37
+ gistore.safe_system(*cmds)
38
+ else
39
+ gistore.setup_environment
40
+ Dir.chdir(work_tree) do
41
+ `#{git_cmd} archive --format=tar #{options[:rev] || 'HEAD'} #{args.map{|e| e.to_s.gsub " ", "\\ "} * " "} | tar xf -`
42
+ end
43
+ end
44
+
45
+ rescue Exception => e
46
+ Tty.die "#{e.message}"
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,171 @@
1
+ require 'gistore/error'
2
+
3
+ module Gistore
4
+ class Runner
5
+ map ["ci", "backup"] => :commit
6
+ desc "commit [-m <message>]", "Start commit changes (i.e. backup)"
7
+ option :message, :aliases => :m, :desc => "commit log"
8
+ def commit(*args)
9
+ parse_common_options_and_repo
10
+
11
+ # Check if backup needs rotate
12
+ gistore.backup_rotate
13
+
14
+ # Compare with last backup, and remove unwanted from cache
15
+ latest_backups = gistore.get_backups
16
+ last_backups = gistore.get_last_backups
17
+ if last_backups
18
+ last_backups.each do |entry|
19
+ if entry and not latest_backups.include? entry
20
+ cmds = [git_cmd,
21
+ "rm",
22
+ "--cached",
23
+ "-r",
24
+ "-f",
25
+ "--ignore-unmatch",
26
+ "--quiet",
27
+ "--",
28
+ entry.sub(/^\/+/, '')]
29
+ cmds << {:check_return => false, :without_work_tree => true}
30
+ gistore.shellout(*cmds)
31
+ end
32
+ end
33
+ end
34
+
35
+ # Add/remove files...
36
+ latest_backups.each do |entry|
37
+ # entry may be ignored by ".gitignore" under parent dirs.
38
+ gistore.shellout git_cmd, "add", "-f", entry.sub(/^\/+/, '')
39
+ end
40
+
41
+ cmds = [git_cmd, "add", "-A"]
42
+ cmds << ":/" if git_version_compare('1.7.6') >= 0
43
+ gistore.shellout *cmds
44
+
45
+ # Read status
46
+ git_status = []
47
+ gistore.shellout git_cmd, "status", "--porcelain" do |stdout|
48
+ stdout.readlines.each do |line|
49
+ line.strip!
50
+ git_status << line unless line.empty?
51
+ end
52
+ end
53
+
54
+ # Add contents of a submodule, not add as a submodule
55
+ submodules = gistore.remove_submodules
56
+ until submodules.empty? do
57
+ Tty.debug "Re-add files in submodules: #{submodules.join(', ')}"
58
+ submodules.each do |submod|
59
+ git_status += gistore.add_submodule(submod)
60
+ end
61
+ # new add directories may contain other submodule.
62
+ submodules = gistore.remove_submodules
63
+ end
64
+
65
+ # Format commit messages
66
+ message = ""
67
+ message << options[:message].strip if options[:message]
68
+ message << "\n\n" unless message.empty?
69
+ message << commit_summary(git_status)
70
+ msgfile = File.join(gistore.repo_path, "COMMIT_EDITMSG")
71
+ open(msgfile, "w") do |io|
72
+ io.puts message
73
+ end
74
+
75
+ # Start to commit
76
+ committed = nil
77
+ output = ""
78
+ begin
79
+ gistore.shellout(git_cmd, "commit", "-s", "--quiet", "-F", msgfile,
80
+ :without_locale => true,
81
+ :check_return => true) do |stdout|
82
+ output = stdout.read
83
+ end
84
+ committed = true
85
+ rescue CommandReturnError
86
+ if output and
87
+ (output =~ /no changes added to commit/ or
88
+ output =~ /nothing to commit/ or
89
+ output =~ /nothing added to commit/)
90
+ committed = false
91
+ else
92
+ raise "Failed to execute git-commit:\n\n#{output.to_s}"
93
+ end
94
+ end
95
+
96
+ # Save backups
97
+ gistore.update_gistore_config(:backups => latest_backups)
98
+ gistore.save_gistore_config
99
+
100
+ display_name = gistore.task_name ?
101
+ "#{gistore.task_name} (#{gistore.repo_path})" :
102
+ "#{gistore.repo_path}"
103
+
104
+ if committed
105
+ Tty.info "Successfully backup repo: #{display_name}"
106
+ # Run git-gc
107
+ gistore.git_gc
108
+ else
109
+ Tty.info "Nothing changed for repo: #{display_name}"
110
+ end
111
+
112
+ rescue Exception => e
113
+ Tty.die "#{e.message}"
114
+ end
115
+
116
+ map ["ci_all", "ci-all", "backup_all", "backup-all"] => :commit_all
117
+ desc "commit-all [-m <message>]", "Start backup (commit) all tasks", :hide => true
118
+ option :message, :aliases => :m, :desc => "commit log"
119
+ def commit_all
120
+ messages = []
121
+ Gistore::get_gistore_tasks.each do |task, path|
122
+ cmds = ["commit", "--repo", path]
123
+ if options[:message]
124
+ cmds << "-m"
125
+ cmds << options[:message]
126
+ end
127
+ # invoke run only once? -- invoke :commit, args, opts
128
+ begin
129
+ Gistore::Runner.start(cmds)
130
+ rescue Exception => e
131
+ messages << "Failed to execute #{cmds}."
132
+ messages << "Error_msg: #{e.message}"
133
+ end
134
+ end
135
+ unless messages.empty?
136
+ Tty.die "At lease one task backup failed.\n#{messages * "\n"}"
137
+ end
138
+ end
139
+
140
+ private
141
+ def commit_summary(git_status)
142
+ sample = 2
143
+ statistics = {}
144
+ output = []
145
+ git_status.each do |line|
146
+ k,v = line.split(" ", 2)
147
+ statistics[k] ||= []
148
+ statistics[k] << v
149
+ end
150
+
151
+ total = git_status.size
152
+ detail = statistics.to_a.map{|h| "#{h[0]}: #{h[1].size}"}.sort.join(", ")
153
+ output << "Backup #{total} item#{total > 1 ? "s" : ""} (#{detail})"
154
+ output << ""
155
+ statistics.keys.sort.each do |k|
156
+ buffer = []
157
+ if statistics[k].size > sample
158
+ step = statistics[k].size / sample
159
+ (0...sample).each do |i|
160
+ buffer << statistics[k][step * i]
161
+ end
162
+ buffer << "...#{statistics[k].size - sample} more..."
163
+ else
164
+ buffer = statistics[k]
165
+ end
166
+ output << " #{k} => #{buffer.join(", ")}"
167
+ end
168
+ output.join("\n")
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,23 @@
1
+ module Gistore
2
+ class Runner
3
+ map "config" => :cmd_config
4
+ desc "config name value", "Read or update gistore config or git config"
5
+ option :plan, :desc => "builtin plan: no-gc, no-compress, or normal (default)"
6
+ def cmd_config(*args)
7
+ parse_common_options_and_repo
8
+ if options[:plan]
9
+ return gistore.git_config('--plan', options[:plan])
10
+ end
11
+
12
+ args << {:check_return => true}
13
+ unless gistore.git_config(*args)
14
+ exit 1
15
+ end
16
+ rescue SystemExit
17
+ exit 1
18
+ rescue Exception => e
19
+ Tty.die "#{e.message}"
20
+ end
21
+ end
22
+ end
23
+
@@ -0,0 +1,79 @@
1
+ module Gistore
2
+ class Runner
3
+ desc "export-to-backups", "Export to a series of full/increment backups"
4
+ option :to, :required => true, :banner => "<dir>", :desc => "path to save full/increment backups"
5
+ def export_to_backups
6
+ parse_common_options_and_repo
7
+ work_tree = options[:to]
8
+ if File.exist? work_tree
9
+ if not File.directory? work_tree
10
+ raise "\"#{work_tree}\" is not a valid directory."
11
+ elsif Dir.entries(work_tree).size != 2
12
+ Tty.warning "\"#{work_tree}\" is not a blank directory."
13
+ end
14
+ else
15
+ require 'fileutils'
16
+ FileUtils.mkdir_p work_tree
17
+ end
18
+
19
+ commits = []
20
+ gistore.shellout(git_cmd, "rev-list", "master",
21
+ :without_grafts => true) do |stdout|
22
+ commits = stdout.readlines
23
+ end
24
+
25
+ export_commits(commits, work_tree)
26
+ rescue Exception => e
27
+ Tty.die "#{e.message}"
28
+ end
29
+
30
+ private
31
+
32
+ def export_commits(commits, work_tree)
33
+ left = right = nil
34
+ n = 1
35
+ until commits.empty?
36
+ right=commits.pop.strip
37
+ export_one_commit(n, left, right, work_tree)
38
+ left = right
39
+ n += 1
40
+ end
41
+ end
42
+
43
+ def export_one_commit(n, left, right, work_tree)
44
+ time = nil
45
+ gistore.shellout(git_cmd, "cat-file", "commit", right) do |stdout|
46
+ stdout.readlines.each do |line|
47
+ if line =~ /^committer .* ([0-9]+)( [+-][0-9]*)?$/
48
+ time = Time.at($1.to_i).strftime("%Y%m%d-%H%M%S")
49
+ break
50
+ end
51
+ end
52
+ end
53
+
54
+ prefix = "%03d-" % n
55
+ prefix << (left ? "incremental" : "full-backup")
56
+ prefix << "-#{time}" if time
57
+ prefix << "-g#{right[0...7]}"
58
+
59
+ if not Dir.glob("#{work_tree}/#{prefix}*.pack").empty?
60
+ Tty.info "already export commit #{right}"
61
+ return
62
+ end
63
+
64
+ if left
65
+ input_rev = "#{left}..#{right}"
66
+ else
67
+ input_rev = right
68
+ end
69
+
70
+ gistore.shellpipe(git_cmd, "pack-objects", "--revs", prefix,
71
+ :without_grafts => true,
72
+ :work_tree => work_tree) do |stdin, stdout, stderr|
73
+ stdin.write input_rev
74
+ stdin.close_write
75
+ end
76
+ Tty.info "export #{n}: #{prefix}"
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,15 @@
1
+ module Gistore
2
+ class Runner
3
+ desc "gc [--force]", "Run git-gc if gc.auto != 0"
4
+ option :force, :type => :boolean, :aliases => [:f], :desc => "run git-gc without --auto option"
5
+ def gc(*args)
6
+ parse_common_options_and_repo
7
+ opts = options.dup
8
+ opts.delete :repo
9
+ args << opts
10
+ gistore.git_gc(*args)
11
+ rescue Exception => e
12
+ Tty.die "#{e.message}"
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,14 @@
1
+ require 'gistore/utils'
2
+
3
+ module Gistore
4
+ class Runner
5
+ desc "check-git-version", "Check git version", :hide => true
6
+ def check_git_version(v1, v2=nil)
7
+ if v2
8
+ puts Gistore.git_version_compare(v1, v2)
9
+ else
10
+ puts Gistore.git_version_compare(v1)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,36 @@
1
+ require 'gistore/repo'
2
+ require 'gistore/utils'
3
+ require 'gistore/version'
4
+
5
+ module Gistore
6
+ class Runner
7
+ desc "init", "Initialize gistore repository"
8
+ long_desc <<-LONGDESC
9
+ `gistore init [--repo] <repo>` will create a gistore backup repo.
10
+
11
+ The created <repo> is a bare git repository, and when excute backup and/or
12
+ other commands on <repo>, GIT_WORK_TREE will be set as '/', and GIT_DIR
13
+ will be set as <repo> automatically.
14
+
15
+ This bare repo has been set with default settings which are suitable for
16
+ backup for text files. But if most of the backups are binaries, you may
17
+ like to set <repo> with custom settings. You can give specific plan for
18
+ <repo> when initializing, like:
19
+
20
+ > $ gistore init --plan <no-gc|no-compress|normal> <repo>
21
+
22
+ Or run `gistore config` command latter, like
23
+
24
+ > $ gistore config --repo <repo> --plan no-compress
25
+ \x5> $ gistore config --repo <repo> --plan no-gc
26
+ LONGDESC
27
+ option :plan, :required => false, :type => :string,
28
+ :desc => "no-gc, no-compress, or normal (default)"
29
+ def init(name=nil)
30
+ parse_common_options
31
+ Repo.init(options[:repo] || name || ".", options)
32
+ rescue Exception => e
33
+ Tty.die "#{e.message}"
34
+ end
35
+ end
36
+ end