git_tree 0.3.0 → 1.0.1

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