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.
- checksums.yaml +7 -0
- data/CHANGELOG +30 -0
- data/COPYING +340 -0
- data/README.md +98 -0
- data/exe/gistore +15 -0
- data/lib/gistore.rb +20 -0
- data/lib/gistore/cmd/add.rb +15 -0
- data/lib/gistore/cmd/checkout.rb +49 -0
- data/lib/gistore/cmd/commit.rb +171 -0
- data/lib/gistore/cmd/config.rb +23 -0
- data/lib/gistore/cmd/export-to-backups.rb +79 -0
- data/lib/gistore/cmd/gc.rb +15 -0
- data/lib/gistore/cmd/git-version.rb +14 -0
- data/lib/gistore/cmd/init.rb +36 -0
- data/lib/gistore/cmd/restore-from-backups.rb +91 -0
- data/lib/gistore/cmd/rm.rb +15 -0
- data/lib/gistore/cmd/safe-commands.rb +53 -0
- data/lib/gistore/cmd/status.rb +40 -0
- data/lib/gistore/cmd/task.rb +85 -0
- data/lib/gistore/cmd/version.rb +27 -0
- data/lib/gistore/config.rb +13 -0
- data/lib/gistore/config/gistore.yml +1 -0
- data/lib/gistore/error.rb +6 -0
- data/lib/gistore/repo.rb +683 -0
- data/lib/gistore/runner.rb +43 -0
- data/lib/gistore/templates/description +1 -0
- data/lib/gistore/templates/hooks/applypatch-msg.sample +15 -0
- data/lib/gistore/templates/hooks/commit-msg.sample +24 -0
- data/lib/gistore/templates/hooks/post-update.sample +8 -0
- data/lib/gistore/templates/hooks/pre-applypatch.sample +14 -0
- data/lib/gistore/templates/hooks/pre-commit.sample +49 -0
- data/lib/gistore/templates/hooks/pre-push.sample +54 -0
- data/lib/gistore/templates/hooks/pre-rebase.sample +169 -0
- data/lib/gistore/templates/hooks/prepare-commit-msg.sample +36 -0
- data/lib/gistore/templates/hooks/update.sample +128 -0
- data/lib/gistore/templates/info/exclude +6 -0
- data/lib/gistore/utils.rb +382 -0
- data/lib/gistore/version.rb +4 -0
- data/t/Makefile +80 -0
- data/t/README +745 -0
- data/t/aggregate-results.sh +46 -0
- data/t/lib-worktree.sh +76 -0
- data/t/t0000-init.sh +75 -0
- data/t/t0010-config.sh +75 -0
- data/t/t0020-version.sh +32 -0
- data/t/t1000-add-remove.sh +89 -0
- data/t/t1010-status.sh +87 -0
- data/t/t1020-commit.sh +134 -0
- data/t/t1030-commit-and-rotate.sh +266 -0
- data/t/t2000-task-and-commit-all.sh +132 -0
- data/t/t3000-checkout.sh +115 -0
- data/t/t3010-export-and-restore.sh +141 -0
- data/t/test-binary-1.png +0 -0
- data/t/test-binary-2.png +0 -0
- data/t/test-lib-functions.sh +722 -0
- data/t/test-lib.sh +684 -0
- 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
|