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
@@ -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
|