gistore 1.0.0.rc4
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -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
|
+
---
|
data/lib/gistore/repo.rb
ADDED
@@ -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
|