git_tree 0.2.3 → 1.0.0
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 +4 -4
- data/.rubocop.yml +25 -6
- data/CHANGELOG.md +48 -13
- data/README.md +356 -68
- data/exe/git-commitAll +3 -0
- data/exe/git-evars +3 -0
- data/exe/git-exec +3 -0
- data/exe/git-replicate +3 -0
- data/exe/git-update +3 -0
- data/git_tree.gemspec +23 -13
- data/lib/commands/abstract_command.rb +88 -0
- data/lib/commands/git_commit_all.rb +155 -0
- data/lib/commands/git_evars.rb +159 -0
- data/lib/commands/git_exec.rb +104 -0
- data/lib/commands/git_exec_spec.rb +0 -0
- data/lib/commands/git_replicate.rb +91 -0
- data/lib/commands/git_update.rb +113 -0
- data/lib/git_tree/version.rb +1 -1
- data/lib/git_tree.rb +10 -26
- data/lib/util/command_runner.rb +12 -0
- data/lib/{util.rb → util/gem_support.rb} +16 -6
- data/lib/util/git_tree_walker.rb +75 -0
- data/lib/util/git_tree_walker_private.rb +60 -0
- data/lib/util/log.rb +49 -0
- data/lib/util/thread_pool_manager.rb +116 -0
- data/lib/util/zowee_optimizer.rb +136 -0
- metadata +55 -19
- data/bindir/git-tree-evars +0 -5
- data/bindir/git-tree-replicate +0 -5
- data/lib/git_tree_evars.rb +0 -82
- data/lib/git_tree_replicate.rb +0 -86
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require_relative '../util/git_tree_walker'
|
3
|
+
require_relative '../util/log'
|
4
|
+
|
5
|
+
module GitTree
|
6
|
+
class AbstractCommand
|
7
|
+
include Logging
|
8
|
+
|
9
|
+
class << self
|
10
|
+
attr_accessor :allow_empty_args
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(args = ARGV, options: {})
|
14
|
+
@raw_args = args
|
15
|
+
@options = { serial: false }.merge(options)
|
16
|
+
end
|
17
|
+
|
18
|
+
# This method should be called after initialize to parse options
|
19
|
+
# and finalize setup. This makes testing easier by allowing dependency
|
20
|
+
# injection before options are parsed.
|
21
|
+
def setup
|
22
|
+
@args = parse_options(@raw_args)
|
23
|
+
# Show help if no arguments are provided, unless allow_empty_args is set.
|
24
|
+
help if @args.empty? && !self.class.allow_empty_args
|
25
|
+
end
|
26
|
+
|
27
|
+
def run
|
28
|
+
raise NotImplementedError, "#{self.class.name} must implement the 'run' method."
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
# Subclasses must implement this to provide their specific help text.
|
34
|
+
def help
|
35
|
+
raise NotImplementedError, "#{self.class.name} must implement the 'help' method."
|
36
|
+
end
|
37
|
+
|
38
|
+
# Provides a base OptionParser. Subclasses will add their specific options.
|
39
|
+
def parse_options(args)
|
40
|
+
parsed_options = {}
|
41
|
+
parser = OptionParser.new do |opts|
|
42
|
+
opts.on("-h", "--help", "Show this help message and exit.") do
|
43
|
+
help
|
44
|
+
end
|
45
|
+
opts.on("-q", "--quiet", "Suppress normal output, only show errors.") do
|
46
|
+
parsed_options[:verbosity] = QUIET
|
47
|
+
end
|
48
|
+
opts.on("-v", "--verbose", "Increase verbosity. Can be used multiple times (e.g., -v, -vv).") do
|
49
|
+
# This logic is now handled after parsing
|
50
|
+
parsed_options[:verbose_count] ||= 0
|
51
|
+
parsed_options[:verbose_count] += 1
|
52
|
+
end
|
53
|
+
opts.on('-s', "--serial", "Run tasks serially in a single thread in the order specified.") do
|
54
|
+
parsed_options[:serial] = true
|
55
|
+
end
|
56
|
+
yield opts if block_given?
|
57
|
+
end
|
58
|
+
remaining_args = parser.parse(args)
|
59
|
+
|
60
|
+
# Apply parsed verbosity settings
|
61
|
+
if parsed_options[:verbosity] == QUIET
|
62
|
+
Logging.verbosity = QUIET
|
63
|
+
elsif parsed_options[:verbose_count]
|
64
|
+
verbosity_level = case parsed_options[:verbose_count]
|
65
|
+
when 1 then VERBOSE
|
66
|
+
else DEBUG # 2 or more -v flags
|
67
|
+
end
|
68
|
+
Logging.verbosity = verbosity_level
|
69
|
+
parsed_options[:verbose] = verbosity_level
|
70
|
+
end
|
71
|
+
|
72
|
+
# Merge parsed options into existing @options, preserving initial ones.
|
73
|
+
@options.merge!(parsed_options)
|
74
|
+
|
75
|
+
remaining_args
|
76
|
+
end
|
77
|
+
|
78
|
+
protected
|
79
|
+
|
80
|
+
# @param dir [String] path to a git repository
|
81
|
+
# @return [Boolean] true if the repository has changes, false otherwise.
|
82
|
+
def repo_has_changes?(dir)
|
83
|
+
repo = Rugged::Repository.new(dir)
|
84
|
+
repo.status { |_path, _status| return true }
|
85
|
+
false
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'shellwords'
|
3
|
+
require 'timeout'
|
4
|
+
require 'rugged'
|
5
|
+
|
6
|
+
require_relative 'abstract_command'
|
7
|
+
require_relative '../util/git_tree_walker'
|
8
|
+
|
9
|
+
module GitTree
|
10
|
+
class CommitAllCommand < AbstractCommand
|
11
|
+
include Logging
|
12
|
+
|
13
|
+
attr_writer :walker
|
14
|
+
|
15
|
+
self.allow_empty_args = true
|
16
|
+
|
17
|
+
def initialize(args = ARGV, options: {})
|
18
|
+
$PROGRAM_NAME = 'git-commitAll'
|
19
|
+
super
|
20
|
+
# Allow walker to be injected for testing
|
21
|
+
@walker = @options.delete(:walker)
|
22
|
+
end
|
23
|
+
|
24
|
+
def run
|
25
|
+
setup
|
26
|
+
@options[:message] ||= '-'
|
27
|
+
@walker ||= GitTreeWalker.new(@args, options: @options)
|
28
|
+
@walker.process do |dir, thread_id, walker|
|
29
|
+
process_repo(dir, thread_id, walker, @options[:message])
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def help(msg = nil)
|
36
|
+
log(QUIET, "Error: #{msg}\n", :red) if msg
|
37
|
+
log QUIET, <<~END_MSG
|
38
|
+
#{$PROGRAM_NAME} - Recursively commits and pushes changes in all git repositories under the specified roots.
|
39
|
+
If no directories are given, uses default environment variables (#{GitTreeWalker::DEFAULT_ROOTS.join(', ')}) as roots.
|
40
|
+
Skips directories containing a .ignore file, and all subdirectories.
|
41
|
+
Repositories in a detached HEAD state are skipped.
|
42
|
+
|
43
|
+
Options:
|
44
|
+
-h, --help Show this help message and exit.
|
45
|
+
-m, --message MESSAGE Use the given string as the commit message.
|
46
|
+
(default: "-")
|
47
|
+
-q, --quiet Suppress normal output, only show errors.
|
48
|
+
-s, --serial Run tasks serially in a single thread in the order specified.
|
49
|
+
-v, --verbose Increase verbosity. Can be used multiple times (e.g., -v, -vv).
|
50
|
+
|
51
|
+
Usage:
|
52
|
+
#{$PROGRAM_NAME} [OPTIONS] [DIRECTORY...]
|
53
|
+
|
54
|
+
Usage examples:
|
55
|
+
#{$PROGRAM_NAME} # Commit with default message "-"
|
56
|
+
#{$PROGRAM_NAME} -m "This is a commit message" # Commit with a custom message
|
57
|
+
#{$PROGRAM_NAME} $work $sites # Commit in repositories under specific roots
|
58
|
+
END_MSG
|
59
|
+
exit 1
|
60
|
+
end
|
61
|
+
|
62
|
+
# Provides an additional OptionParser to the base OptionParser defined in AbstractCommand.
|
63
|
+
# @param args [Array<String>] The remaining command-line arguments after the AbstractCommand OptionParser has been applied.
|
64
|
+
# @return [nil]
|
65
|
+
def parse_options(args)
|
66
|
+
@args = super do |opts|
|
67
|
+
opts.banner = "Usage: #{$PROGRAM_NAME} [options] [DIRECTORY ...]"
|
68
|
+
opts.on("-m MESSAGE", "--message MESSAGE", "Use the given string as the commit message.") do |m|
|
69
|
+
@options[:message] = m
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Processes a single git repository to check for and commit changes.
|
75
|
+
# @param dir [String] The path to the git repository.
|
76
|
+
# @param thread_id [Integer] The ID of the current worker thread.
|
77
|
+
# @param walker [GitTreeWalker] The GitTreeWalker instance.
|
78
|
+
# @param message [String] The commit message to use.
|
79
|
+
# @return [nil]
|
80
|
+
def process_repo(dir, thread_id, walker, message)
|
81
|
+
short_dir = walker.abbreviate_path(dir)
|
82
|
+
log VERBOSE, "Examining #{short_dir} on thread #{thread_id}", :green
|
83
|
+
begin
|
84
|
+
# The highest priority is to check for the presence of an .ignore file.
|
85
|
+
if File.exist?(File.join(dir, '.ignore'))
|
86
|
+
log DEBUG, " Skipping #{short_dir} due to .ignore file", :green
|
87
|
+
return
|
88
|
+
end
|
89
|
+
|
90
|
+
repo = Rugged::Repository.new(dir)
|
91
|
+
if repo.head_detached?
|
92
|
+
log VERBOSE, " Skipping #{short_dir} because it is in a detached HEAD state", :yellow
|
93
|
+
return
|
94
|
+
end
|
95
|
+
|
96
|
+
Timeout.timeout(GitTreeWalker::GIT_TIMEOUT) do
|
97
|
+
unless repo_has_changes?(dir)
|
98
|
+
log DEBUG, " No changes to commit in #{short_dir}", :green
|
99
|
+
return
|
100
|
+
end
|
101
|
+
commit_changes(dir, message, short_dir)
|
102
|
+
end
|
103
|
+
rescue Timeout::Error
|
104
|
+
log NORMAL, "[TIMEOUT] Thread #{thread_id}: git operations timed out in #{short_dir}", :red
|
105
|
+
rescue StandardError => e
|
106
|
+
log NORMAL, "#{e.class} processing #{short_dir}: #{e.message}", :red
|
107
|
+
e.backtrace.join("\n").each_line { |line| log DEBUG, line, :red }
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# @param dir [String] The path to the git repository.
|
112
|
+
# @return [Boolean] True if the repository has changes, false otherwise.
|
113
|
+
def repo_has_staged_changes?(repo)
|
114
|
+
# For an existing repo, diff the index against the HEAD tree.
|
115
|
+
head_tree = repo.head.target.tree
|
116
|
+
diff = head_tree.diff(repo.index)
|
117
|
+
!diff.deltas.empty?
|
118
|
+
rescue Rugged::ReferenceError # Handles a new repo with no commits yet.
|
119
|
+
# If there's no HEAD, any file in the index is a staged change for the first commit.
|
120
|
+
!repo.index.empty?
|
121
|
+
end
|
122
|
+
|
123
|
+
# @param dir [String] The path to the git repository.
|
124
|
+
# @param message [String] The commit message to use.
|
125
|
+
# @param short_dir [String] The shortened path to the git repository.
|
126
|
+
# @return [nil]
|
127
|
+
def commit_changes(dir, message, short_dir)
|
128
|
+
system('git', '-C', dir, 'add', '--all', exception: true)
|
129
|
+
|
130
|
+
repo = Rugged::Repository.new(dir)
|
131
|
+
return unless repo_has_staged_changes?(repo)
|
132
|
+
|
133
|
+
system('git', '-C', dir, 'commit', '-m', message, '--quiet', '--no-gpg-sign', exception: true)
|
134
|
+
|
135
|
+
# Re-initialize the repo object to get the fresh state after the commit.
|
136
|
+
repo = Rugged::Repository.new(dir)
|
137
|
+
|
138
|
+
current_branch = repo.head.name.sub('refs/heads/', '')
|
139
|
+
system('git', '-C', dir, 'push', '--set-upstream', 'origin', current_branch, exception: true)
|
140
|
+
log NORMAL, "Committed and pushed changes in #{short_dir}", :green
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
if $PROGRAM_NAME == __FILE__ || $PROGRAM_NAME.end_with?('git-commitAll') # Corrected from git-tree-commitAll
|
146
|
+
begin
|
147
|
+
GitTree::CommitAllCommand.new(ARGV).run
|
148
|
+
rescue Interrupt
|
149
|
+
log NORMAL, "\nInterrupted by user", :yellow
|
150
|
+
exit! 130 # Use exit! to prevent further exceptions on shutdown
|
151
|
+
rescue StandardError => e
|
152
|
+
log QUIET, "#{e.class}: #{e.message}\n#{e.backtrace.join("\n")}", :red
|
153
|
+
exit 1
|
154
|
+
end
|
155
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
require_relative '../git_tree'
|
2
|
+
require_relative 'abstract_command'
|
3
|
+
require_relative '../util/git_tree_walker'
|
4
|
+
require_relative '../util/zowee_optimizer'
|
5
|
+
|
6
|
+
module GitTree
|
7
|
+
class EvarsCommand < GitTree::AbstractCommand
|
8
|
+
self.allow_empty_args = true
|
9
|
+
|
10
|
+
def initialize(args = ARGV, options: {})
|
11
|
+
$PROGRAM_NAME = 'git-evars'
|
12
|
+
super
|
13
|
+
end
|
14
|
+
|
15
|
+
def run
|
16
|
+
setup
|
17
|
+
result = []
|
18
|
+
if @options[:zowee]
|
19
|
+
walker = GitTreeWalker.new(@args, options: @options)
|
20
|
+
all_paths = []
|
21
|
+
walker.find_and_process_repos do |dir, _|
|
22
|
+
all_paths << dir
|
23
|
+
end
|
24
|
+
optimizer = ZoweeOptimizer.new(walker.root_map)
|
25
|
+
result = optimizer.optimize(all_paths, walker.display_roots)
|
26
|
+
elsif @args.empty? # No args provided, use default roots and substitute them in the output
|
27
|
+
walker = GitTreeWalker.new([], options: @options)
|
28
|
+
walker.find_and_process_repos do |dir, _root_arg|
|
29
|
+
result << make_env_var_with_substitution(dir, GitTreeWalker::DEFAULT_ROOTS)
|
30
|
+
end
|
31
|
+
else # Args were provided, process them as roots
|
32
|
+
processed_args = @args.flat_map { |arg| arg.strip.split(/\s+/) }
|
33
|
+
processed_args.each { |root| result.concat(process_root(root)) }
|
34
|
+
end
|
35
|
+
log_stdout result.join("\n") unless result.empty?
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
# @param path [String] The path to convert to an environment variable name.
|
41
|
+
# @return [String] The converted environment variable name.
|
42
|
+
def env_var_name(path)
|
43
|
+
name = path.include?('/') ? File.basename(path) : path
|
44
|
+
name.tr(' ', '_').tr('-', '_')
|
45
|
+
end
|
46
|
+
|
47
|
+
# @param msg [String] The error message to display before the help text.
|
48
|
+
# @return [nil]
|
49
|
+
def help(msg = nil)
|
50
|
+
log(QUIET, "Error: #{msg}\n", :red) if msg
|
51
|
+
log QUIET, <<~END_HELP
|
52
|
+
#{$PROGRAM_NAME} - Generate bash environment variables for each git repository found under specified directory trees.
|
53
|
+
|
54
|
+
Examines trees of git repositories and writes a bash script to STDOUT.
|
55
|
+
If no directories are given, uses default environment variables (#{GitTreeWalker::DEFAULT_ROOTS.join(', ')}) as roots.
|
56
|
+
These environment variables point to roots of git repository trees to walk.
|
57
|
+
Skips directories containing a .ignore file, and all subdirectories.
|
58
|
+
|
59
|
+
Does not redefine existing environment variables; messages are written to STDERR to indicate environment variables that are not redefined.
|
60
|
+
|
61
|
+
Environment variables that point to the roots of git repository trees must have been exported, for example:
|
62
|
+
|
63
|
+
$ export work=$HOME/work
|
64
|
+
|
65
|
+
Usage: #{$PROGRAM_NAME} [OPTIONS] [ROOTS...]
|
66
|
+
|
67
|
+
Options:
|
68
|
+
-h, --help Show this help message and exit.
|
69
|
+
-q, --quiet Suppress normal output, only show errors.
|
70
|
+
-z, --zowee Optimize variable definitions for size.
|
71
|
+
-v, --verbose Increase verbosity. Can be used multiple times (e.g., -v, -vv).
|
72
|
+
|
73
|
+
ROOTS can be directory names or environment variable references enclosed within single quotes (e.g., '$work').
|
74
|
+
Multiple roots can be specified in a single quoted string.
|
75
|
+
|
76
|
+
Usage examples:
|
77
|
+
$ #{$PROGRAM_NAME} # Use default environment variables as roots
|
78
|
+
$ #{$PROGRAM_NAME} '$work $sites' # Use specific environment variables
|
79
|
+
END_HELP
|
80
|
+
exit 1
|
81
|
+
end
|
82
|
+
|
83
|
+
# @param args [Array<String>] The command-line arguments.
|
84
|
+
# @return [Array<String>] The parsed options.
|
85
|
+
def parse_options(args)
|
86
|
+
@args = super do |opts|
|
87
|
+
opts.on("-z", "--zowee", "Optimize variable definitions for size.") do
|
88
|
+
@options[:zowee] = true
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# @param root [String] The root environment variable reference (e.g., '$work').
|
94
|
+
# @return [Array<String>] An array of environment variable definitions.
|
95
|
+
def process_root(root)
|
96
|
+
help("Environment variable reference must start with a dollar sign ($).") unless root.start_with? '$'
|
97
|
+
|
98
|
+
base = GemSupport.expand_env(root)
|
99
|
+
help("Environment variable '#{root}' is undefined.") if base.nil? || base.strip.empty?
|
100
|
+
help("Environment variable '#{root}' points to a non-existant directory (#{base}).") unless File.exist?(base)
|
101
|
+
help("Environment variable '#{root}' points to a file (#{base}), not a directory.") unless Dir.exist?(base)
|
102
|
+
|
103
|
+
result = [make_env_var(env_var_name(base), GemSupport.deref_symlink(base))]
|
104
|
+
walker = GitTreeWalker.new([root], options: @options)
|
105
|
+
walker.find_and_process_repos do |dir|
|
106
|
+
relative_dir = dir.sub(base + '/', '')
|
107
|
+
result << make_env_var(env_var_name(relative_dir), "#{root}/#{relative_dir}")
|
108
|
+
end
|
109
|
+
result
|
110
|
+
end
|
111
|
+
|
112
|
+
# @param name [String] The name of the environment variable.
|
113
|
+
# @param value [String] The value of the environment variable.
|
114
|
+
# @return [String] The environment variable definition string.
|
115
|
+
def make_env_var(name, value)
|
116
|
+
"export #{env_var_name(name)}=#{value}"
|
117
|
+
end
|
118
|
+
|
119
|
+
# Find which root this dir belongs to and substitute it.
|
120
|
+
# @param dir [String] The directory path to process.
|
121
|
+
# @param roots [Array<String>] An array of root environment variable names (e.g., ['work', 'sites']).
|
122
|
+
# @return [String] The environment variable definition string, or nil if no root matches.
|
123
|
+
def make_env_var_with_substitution(dir, roots)
|
124
|
+
found_root_var = nil
|
125
|
+
found_root_path = nil
|
126
|
+
|
127
|
+
roots.each do |root_name|
|
128
|
+
root_path = ENV.fetch(root_name, nil)
|
129
|
+
next if root_path.nil? || root_path.strip.empty?
|
130
|
+
|
131
|
+
next unless dir.start_with?(root_path)
|
132
|
+
|
133
|
+
found_root_var = "$#{root_name}"
|
134
|
+
found_root_path = root_path
|
135
|
+
break
|
136
|
+
end
|
137
|
+
|
138
|
+
if found_root_var
|
139
|
+
relative_dir = dir.sub(found_root_path + '/', '')
|
140
|
+
make_env_var(env_var_name(relative_dir), "#{found_root_var}/#{relative_dir}")
|
141
|
+
else
|
142
|
+
# Fallback to absolute path if no root matches (should be rare).
|
143
|
+
make_env_var(env_var_name(dir), dir)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
if $PROGRAM_NAME == __FILE__ || $PROGRAM_NAME.end_with?('git-evars')
|
150
|
+
begin
|
151
|
+
GitTree::EvarsCommand.new(ARGV).run
|
152
|
+
rescue Interrupt
|
153
|
+
log NORMAL, "\nInterrupted by user", :yellow
|
154
|
+
exit! 130 # Use exit! to prevent further exceptions on shutdown
|
155
|
+
rescue StandardError => e
|
156
|
+
log QUIET, "#{e.class}: #{e.message}\n#{e.backtrace.join("\n")}", :red
|
157
|
+
exit! 1
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require_relative 'abstract_command'
|
3
|
+
require_relative '../util/git_tree_walker'
|
4
|
+
require_relative '../util/thread_pool_manager'
|
5
|
+
require_relative '../util/command_runner'
|
6
|
+
|
7
|
+
module GitTree
|
8
|
+
class ExecCommand < GitTree::AbstractCommand
|
9
|
+
attr_writer :walker, :runner
|
10
|
+
|
11
|
+
def initialize(args = ARGV, options: {})
|
12
|
+
$PROGRAM_NAME = 'git-exec'
|
13
|
+
super
|
14
|
+
# Allow walker and runner to be injected for testing
|
15
|
+
@runner = @options.delete(:runner)
|
16
|
+
@walker = @options.delete(:walker)
|
17
|
+
end
|
18
|
+
|
19
|
+
def run
|
20
|
+
setup
|
21
|
+
return help('At least one root and a command must be specified.') if @args.length < 2
|
22
|
+
|
23
|
+
@runner ||= CommandRunner.new
|
24
|
+
# The last argument is the command to execute, the rest are roots for the walker.
|
25
|
+
@walker ||= GitTreeWalker.new(@args[0..-2], options: @options)
|
26
|
+
|
27
|
+
command = @args.last
|
28
|
+
@walker.process do |dir, _thread_id, _walker|
|
29
|
+
execute_and_log(dir, command)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def execute_and_log(dir, command)
|
36
|
+
output, status = @runner.run(command, dir)
|
37
|
+
log_result(output, status.success?)
|
38
|
+
rescue StandardError => e
|
39
|
+
error_message = "Error: '#{e.message}' from executing '#{command}' in #{dir}"
|
40
|
+
log_result(error_message, false)
|
41
|
+
end
|
42
|
+
|
43
|
+
def log_result(output, success)
|
44
|
+
return if output.strip.empty?
|
45
|
+
|
46
|
+
if success
|
47
|
+
# Successful command output should go to STDOUT.
|
48
|
+
log_stdout output.strip
|
49
|
+
else
|
50
|
+
# Errors should go to STDERR.
|
51
|
+
log QUIET, output.strip, :red
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def help(msg = nil)
|
56
|
+
log(QUIET, "Error: #{msg}\n", :red) if msg
|
57
|
+
log QUIET, <<~END_HELP
|
58
|
+
#{$PROGRAM_NAME} - Executes an arbitrary shell command for each repository.
|
59
|
+
|
60
|
+
If no arguments are given, uses default environment variables (#{GitTreeWalker::DEFAULT_ROOTS.join(', ')}) as roots.
|
61
|
+
These environment variables point to roots of git repository trees to walk.
|
62
|
+
Skips directories containing a .ignore file, and all subdirectories.
|
63
|
+
|
64
|
+
Environment variables that point to the roots of git repository trees must have been exported, for example:
|
65
|
+
|
66
|
+
$ export work=$HOME/work
|
67
|
+
|
68
|
+
Usage: #{$PROGRAM_NAME} [OPTIONS] [ROOTS...] SHELL_COMMAND
|
69
|
+
|
70
|
+
Options:
|
71
|
+
-h, --help Show this help message and exit.
|
72
|
+
-q, --quiet Suppress normal output, only show errors.
|
73
|
+
-s, --serial Run tasks serially in a single thread in the order specified.
|
74
|
+
-v, --verbose Increase verbosity. Can be used multiple times (e.g., -v, -vv).
|
75
|
+
|
76
|
+
ROOTS can be directory names or environment variable references (e.g., '$work').
|
77
|
+
Multiple roots can be specified in a single quoted string.
|
78
|
+
|
79
|
+
Usage examples:
|
80
|
+
1) For all git repositories under $sites, display their root directories:
|
81
|
+
$ #{$PROGRAM_NAME} '$sites' pwd
|
82
|
+
|
83
|
+
2) For all git repositories under the current directory and $my_plugins, list the `demo/` subdirectory if it exists.
|
84
|
+
$ #{$PROGRAM_NAME} '. $my_plugins' 'if [ -d demo ]; then realpath demo; fi'
|
85
|
+
|
86
|
+
3) For all subdirectories of the current directory, update Gemfile.lock and install a local copy of the gem:
|
87
|
+
$ #{$PROGRAM_NAME} . 'bundle update && rake install'
|
88
|
+
END_HELP
|
89
|
+
exit 1
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
if $PROGRAM_NAME == __FILE__ || $PROGRAM_NAME.end_with?('git-exec')
|
95
|
+
begin
|
96
|
+
GitTree::ExecCommand.new(ARGV).run
|
97
|
+
rescue Interrupt
|
98
|
+
log NORMAL, "\nInterrupted by user", :yellow
|
99
|
+
exit! 130 # Use exit! to prevent further exceptions on shutdown
|
100
|
+
rescue StandardError => e
|
101
|
+
log QUIET, "#{e.class}: #{e.message}\n#{e.backtrace.join("\n")}", :red
|
102
|
+
exit 1
|
103
|
+
end
|
104
|
+
end
|
File without changes
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require_relative '../git_tree'
|
2
|
+
require_relative 'abstract_command'
|
3
|
+
require_relative '../util/git_tree_walker'
|
4
|
+
|
5
|
+
module GitTree
|
6
|
+
class ReplicateCommand < GitTree::AbstractCommand
|
7
|
+
self.allow_empty_args = true
|
8
|
+
|
9
|
+
def initialize(args = ARGV, options: {})
|
10
|
+
$PROGRAM_NAME = 'git-replicate'
|
11
|
+
super
|
12
|
+
end
|
13
|
+
|
14
|
+
# @return [nil]
|
15
|
+
def run
|
16
|
+
setup
|
17
|
+
result = []
|
18
|
+
walker = GitTreeWalker.new(@args, options: @options)
|
19
|
+
walker.find_and_process_repos { |dir, root_arg| result << replicate_one(dir, root_arg) }
|
20
|
+
log_stdout result.join("\n") unless result.empty?
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def help(msg = nil)
|
26
|
+
log(QUIET, "Error: #{msg}\n", :red) if msg
|
27
|
+
log QUIET, <<~END_HELP
|
28
|
+
#{$PROGRAM_NAME} - Replicates trees of git repositories and writes a bash script to STDOUT.
|
29
|
+
If no directories are given, uses default environment variables (#{GitTreeWalker::DEFAULT_ROOTS.join(', ')}) as roots.
|
30
|
+
The script clones the repositories and replicates any remotes.
|
31
|
+
Skips directories containing a .ignore file.
|
32
|
+
|
33
|
+
Options:
|
34
|
+
-h, --help Show this help message and exit.
|
35
|
+
-q, --quiet Suppress normal output, only show errors.
|
36
|
+
-v, --verbose Increase verbosity. Can be used multiple times (e.g., -v, -vv).
|
37
|
+
|
38
|
+
Usage: #{$PROGRAM_NAME} [OPTIONS] [ROOTS...]
|
39
|
+
|
40
|
+
ROOTS can be directory names or environment variable references (e.g., '$work').
|
41
|
+
Multiple roots can be specified in a single quoted string.
|
42
|
+
|
43
|
+
Usage examples:
|
44
|
+
$ #{$PROGRAM_NAME} '$work'
|
45
|
+
$ #{$PROGRAM_NAME} '$work $sites'
|
46
|
+
END_HELP
|
47
|
+
exit! 1
|
48
|
+
end
|
49
|
+
|
50
|
+
def replicate_one(dir, root_arg)
|
51
|
+
output = []
|
52
|
+
config_path = File.join(dir, '.git', 'config')
|
53
|
+
return output unless File.exist?(config_path)
|
54
|
+
|
55
|
+
config = Rugged::Config.new(config_path)
|
56
|
+
origin_url = config['remote.origin.url']
|
57
|
+
return output unless origin_url
|
58
|
+
|
59
|
+
base_path = File.expand_path(ENV.fetch(root_arg.tr("'$", ''), ''))
|
60
|
+
relative_dir = dir.sub(base_path + '/', '')
|
61
|
+
|
62
|
+
output << "if [ ! -d \"#{relative_dir}/.git\" ]; then"
|
63
|
+
output << " mkdir -p '#{File.dirname(relative_dir)}'"
|
64
|
+
output << " pushd '#{File.dirname(relative_dir)}' > /dev/null"
|
65
|
+
output << " git clone '#{origin_url}' '#{File.basename(relative_dir)}'"
|
66
|
+
config.each_key do |key|
|
67
|
+
next unless key.start_with?('remote.') && key.end_with?('.url')
|
68
|
+
|
69
|
+
remote_name = key.split('.')[1]
|
70
|
+
next if remote_name == 'origin'
|
71
|
+
|
72
|
+
output << " git remote add #{remote_name} '#{config[key]}'"
|
73
|
+
end
|
74
|
+
output << ' popd > /dev/null'
|
75
|
+
output << 'fi'
|
76
|
+
output
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
if $PROGRAM_NAME == __FILE__ || $PROGRAM_NAME.end_with?('git-replicate') # Corrected from git-tree-replicate
|
82
|
+
begin
|
83
|
+
GitTree::ReplicateCommand.new(ARGV).run
|
84
|
+
rescue Interrupt
|
85
|
+
log NORMAL, "\nInterrupted by user", :yellow
|
86
|
+
exit! 130 # Use exit! to prevent further exceptions on shutdown
|
87
|
+
rescue StandardError => e
|
88
|
+
log QUIET, "#{e.class}: #{e.message}\n#{e.backtrace.join("\n")}", :red
|
89
|
+
exit! 1
|
90
|
+
end
|
91
|
+
end
|