git_tree 1.0.1 → 1.0.2
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 +3 -0
- data/CHANGELOG.md +6 -0
- data/README.md +56 -0
- data/exe/git-treeconfig +3 -0
- data/git_tree.gemspec +7 -1
- data/lib/commands/abstract_command.rb +33 -56
- data/lib/commands/git_commit_all.rb +22 -15
- data/lib/commands/git_evars.rb +7 -7
- data/lib/commands/git_exec.rb +9 -6
- data/lib/commands/git_replicate.rb +5 -5
- data/lib/commands/git_treeconfig.rb +60 -0
- data/lib/commands/git_update.rb +15 -15
- data/lib/git_tree/version.rb +1 -1
- data/lib/util/config.rb +30 -0
- data/lib/util/git_tree_walker.rb +8 -7
- data/lib/util/log.rb +8 -4
- data/lib/util/thread_pool_manager.rb +8 -7
- metadata +37 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4703e322963fd70470c26e91b28bbc1bee8928abe0b8090ba7eabc5d1970f1d5
|
4
|
+
data.tar.gz: 2fd2b50f103e7b6ea25d917337f1710e31f5ed01c963a5eb07b1917f32fb4a00
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3f6f72f9f8d29388eb116423b34236fc876e3b14de99f2024cec036e562afc9be2bde0973ef78d80728e420d4b5ceedcef3906c0993c156362d89939ee674214
|
7
|
+
data.tar.gz: bb56c09a095f234c0c3b8a9373fe4e8c5cdd13b3935a28a7af6c2b1c671aa94c365b5f5dfacaaf15d0e1a8e7f2e9a09dc18e6c59c8c6a7913c491e30cffa4f8c
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,11 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
+
## 1.0.2 / 2025-10-04
|
4
|
+
|
5
|
+
* Increased `GitTreeWalker.GIT_TIMEOUT` to 5 minutes.
|
6
|
+
* Added `git-treeconfig` command and support for configuration file and environment variable configuration.
|
7
|
+
|
8
|
+
|
3
9
|
## 1.0.1 / 2025-10-04
|
4
10
|
|
5
11
|
* Removed unnecessary and problematic `gem_support` dependency.
|
data/README.md
CHANGED
@@ -47,6 +47,61 @@ $ gem install git_tree
|
|
47
47
|
|
48
48
|
To register the new commands, either log out and log back in, or open a new console.
|
49
49
|
|
50
|
+
|
51
|
+
## Configuration
|
52
|
+
|
53
|
+
The `git_tree` commands can be configured to suit your preferences. Settings are resolved in the following order of precedence,
|
54
|
+
where items higher in the list override those lower down:
|
55
|
+
|
56
|
+
1. **Environment Variables**
|
57
|
+
2. **User Configuration File** (`~/.treeconfig.yml`)
|
58
|
+
3. **Default values** built into the gem.
|
59
|
+
|
60
|
+
This allows for flexible customization of the gem's behavior.
|
61
|
+
|
62
|
+
### Interactive Setup: `git-treeconfig`
|
63
|
+
|
64
|
+
The easiest way to get started is to use the `git-treeconfig` command. This interactive tool will ask you a few questions
|
65
|
+
and create a configuration file for you at `~/.treeconfig.yml`.
|
66
|
+
|
67
|
+
```shell
|
68
|
+
$ git-treeconfig
|
69
|
+
Welcome to git-tree configuration.
|
70
|
+
This utility will help you create a configuration file at: /home/user/.treeconfig.yml
|
71
|
+
Press Enter to accept the default value in brackets.
|
72
|
+
|
73
|
+
Git command timeout in seconds? |300| 600
|
74
|
+
Default verbosity level (0=quiet, 1=normal, 2=verbose)? |1|
|
75
|
+
Default root directories (space-separated)? |sites sitesUbuntu work| dev projects
|
76
|
+
|
77
|
+
Configuration saved to /home/user/.treeconfig.yml
|
78
|
+
```
|
79
|
+
|
80
|
+
### Configuration File
|
81
|
+
|
82
|
+
The `git-treeconfig` command generates a YAML file (`~/.treeconfig.yml`) that you can also edit manually.
|
83
|
+
|
84
|
+
Here is an example:
|
85
|
+
|
86
|
+
```yaml
|
87
|
+
---
|
88
|
+
git_timeout: 600
|
89
|
+
verbosity: 1
|
90
|
+
default_roots:
|
91
|
+
- dev
|
92
|
+
- projects
|
93
|
+
```
|
94
|
+
|
95
|
+
### Environment Variables
|
96
|
+
|
97
|
+
For temporary overrides or use in CI/CD environments, you can use environment variables.
|
98
|
+
They must be prefixed with `GIT_TREE_` and be in uppercase.
|
99
|
+
|
100
|
+
- `export GIT_TREE_GIT_TIMEOUT=900`
|
101
|
+
- `export GIT_TREE_VERBOSITY=2`
|
102
|
+
- `export GIT_TREE_DEFAULT_ROOTS="dev projects personal"` (space-separated string)
|
103
|
+
|
104
|
+
|
50
105
|
## Use Cases
|
51
106
|
|
52
107
|
### Dependent Gem Maintenance
|
@@ -64,6 +119,7 @@ the bash script proved difficult to maintain. This use case is now fulfilled by
|
|
64
119
|
provided by the `git_tree` gem.
|
65
120
|
See below for further details.
|
66
121
|
|
122
|
+
|
67
123
|
### Replicating Trees of Git Repositories
|
68
124
|
|
69
125
|
Whenever I set up an operating system for a new development computer,
|
data/exe/git-treeconfig
ADDED
data/git_tree.gemspec
CHANGED
@@ -21,7 +21,7 @@ Gem::Specification.new do |spec| # rubocop:disable Metrics/BlockLength
|
|
21
21
|
The git-update command updates each repository in the tree.
|
22
22
|
END_OF_DESC
|
23
23
|
spec.email = ['mslinn@mslinn.com']
|
24
|
-
spec.executables = %w[git-commitAll git-evars git-exec git-replicate git-update]
|
24
|
+
spec.executables = %w[git-commitAll git-evars git-exec git-replicate git-treeconfig git-update]
|
25
25
|
spec.files = Dir[
|
26
26
|
'{exe,lib}/**/*',
|
27
27
|
'.rubocop.yml',
|
@@ -45,11 +45,17 @@ Gem::Specification.new do |spec| # rubocop:disable Metrics/BlockLength
|
|
45
45
|
|
46
46
|
Thanks for installing #{spec.name}!
|
47
47
|
|
48
|
+
To customize default settings like timeout and repository roots,
|
49
|
+
run the interactive configuration tool:
|
50
|
+
git-treeconfig
|
51
|
+
|
48
52
|
END_MESSAGE
|
49
53
|
spec.required_ruby_version = '>= 3.2.0'
|
50
54
|
spec.summary = 'Installs five commands that walk a git directory tree and perform tasks.'
|
51
55
|
spec.version = GitUrlsVersion::VERSION
|
52
56
|
|
57
|
+
spec.add_dependency 'anyway_config', '~> 2.0'
|
58
|
+
spec.add_dependency 'highline'
|
53
59
|
spec.add_dependency 'rainbow'
|
54
60
|
spec.add_dependency 'rugged'
|
55
61
|
end
|
@@ -1,8 +1,10 @@
|
|
1
1
|
require 'optparse'
|
2
|
-
require_relative '../util/
|
2
|
+
require_relative '../util/config'
|
3
3
|
require_relative '../util/log'
|
4
4
|
|
5
5
|
module GitTree
|
6
|
+
# Abstract base class for all git-tree commands.
|
7
|
+
# It handles common option parsing for verbosity and help.
|
6
8
|
class AbstractCommand
|
7
9
|
include Logging
|
8
10
|
|
@@ -11,78 +13,53 @@ module GitTree
|
|
11
13
|
end
|
12
14
|
|
13
15
|
def initialize(args = ARGV, options: {})
|
14
|
-
@
|
15
|
-
@options =
|
16
|
+
@args = args
|
17
|
+
@options = options
|
18
|
+
@config = GitTree::Config.new
|
19
|
+
# Set initial verbosity from config before anything else happens.
|
20
|
+
# log Logging::VERBOSE, "AbstractCommand#initialize: Setting initial verbosity from config to: #{@config.verbosity}"
|
21
|
+
Logging.verbosity = @config.verbosity
|
16
22
|
end
|
17
23
|
|
18
|
-
#
|
19
|
-
#
|
20
|
-
# injection before options are parsed.
|
24
|
+
# Common setup for all commands.
|
25
|
+
# Parses options and sets initial verbosity.
|
21
26
|
def setup
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
end
|
26
|
-
|
27
|
-
def run
|
28
|
-
raise NotImplementedError, "#{self.class.name} must implement the 'run' method."
|
27
|
+
# CLI options can override the config verbosity.
|
28
|
+
log Logging::VERBOSE, "AbstractCommand#setup: verbosity before parsing options: #{Logging.verbosity}"
|
29
|
+
parse_options(@args)
|
29
30
|
end
|
30
31
|
|
31
32
|
private
|
32
33
|
|
33
|
-
#
|
34
|
-
|
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.
|
34
|
+
# Parses common options like -h, -q, -v.
|
35
|
+
# This method can be extended by subclasses by passing a block.
|
39
36
|
def parse_options(args)
|
40
|
-
parsed_options = {}
|
41
37
|
parser = OptionParser.new do |opts|
|
42
|
-
opts.on("-h", "--help", "Show this help message and exit
|
38
|
+
opts.on("-h", "--help", "Show this help message and exit") do
|
43
39
|
help
|
44
40
|
end
|
45
|
-
|
46
|
-
|
41
|
+
|
42
|
+
opts.on("-q", "--quiet", "Suppress normal output, only show errors") do
|
43
|
+
log Logging::NORMAL, "OptionParser: -q setting verbosity to QUIET"
|
44
|
+
Logging.verbosity = ::Logging::QUIET
|
47
45
|
end
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
46
|
+
|
47
|
+
opts.on("-s", "--serial", "Run tasks serially in a single thread") do
|
48
|
+
@options[:serial] = true
|
49
|
+
log Logging::NORMAL, "OptionParser: -s setting serial mode"
|
52
50
|
end
|
53
|
-
|
54
|
-
|
51
|
+
|
52
|
+
opts.on("-v", "--verbose", "Increase verbosity. Can be used multiple times.") do
|
53
|
+
Logging.verbosity += 1
|
54
|
+
log Logging::NORMAL, "OptionParser: -v increased verbosity to #{Logging.verbosity}"
|
55
55
|
end
|
56
|
-
yield opts if block_given?
|
57
|
-
end
|
58
|
-
remaining_args = parser.parse(args)
|
59
56
|
|
60
|
-
|
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
|
57
|
+
yield(opts) if block_given?
|
70
58
|
end
|
71
59
|
|
72
|
-
|
73
|
-
|
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
|
60
|
+
parser.parse!(args)
|
61
|
+
help("No arguments are allowed") if !self.class.allow_empty_args && args.empty?
|
62
|
+
args
|
86
63
|
end
|
87
64
|
end
|
88
65
|
end
|
@@ -4,7 +4,7 @@ require 'timeout'
|
|
4
4
|
require 'rugged'
|
5
5
|
|
6
6
|
require_relative 'abstract_command'
|
7
|
-
require_relative '../util/git_tree_walker'
|
7
|
+
require_relative '../util/git_tree_walker' # This is correct, no change needed here.
|
8
8
|
|
9
9
|
module GitTree
|
10
10
|
class CommitAllCommand < AbstractCommand
|
@@ -33,10 +33,10 @@ module GitTree
|
|
33
33
|
private
|
34
34
|
|
35
35
|
def help(msg = nil)
|
36
|
-
log(QUIET, "Error: #{msg}\n", :red) if msg
|
37
|
-
log QUIET, <<~END_MSG
|
36
|
+
log(Logging::QUIET, "Error: #{msg}\n", :red) if msg
|
37
|
+
log Logging::QUIET, <<~END_MSG
|
38
38
|
#{$PROGRAM_NAME} - Recursively commits and pushes changes in all git repositories under the specified roots.
|
39
|
-
If no directories are given, uses default
|
39
|
+
If no directories are given, uses default roots (#{@config.default_roots.join(', ')}) as roots.
|
40
40
|
Skips directories containing a .ignore file, and all subdirectories.
|
41
41
|
Repositories in a detached HEAD state are skipped.
|
42
42
|
|
@@ -79,35 +79,42 @@ module GitTree
|
|
79
79
|
# @return [nil]
|
80
80
|
def process_repo(dir, thread_id, walker, message)
|
81
81
|
short_dir = walker.abbreviate_path(dir)
|
82
|
-
log VERBOSE, "Examining #{short_dir} on thread #{thread_id}", :green
|
82
|
+
log Logging::VERBOSE, "Examining #{short_dir} on thread #{thread_id}", :green
|
83
83
|
begin
|
84
84
|
# The highest priority is to check for the presence of an .ignore file.
|
85
85
|
if File.exist?(File.join(dir, '.ignore'))
|
86
|
-
log DEBUG, " Skipping #{short_dir} due to .ignore file", :green
|
86
|
+
log Logging::DEBUG, " Skipping #{short_dir} due to .ignore file", :green
|
87
87
|
return
|
88
88
|
end
|
89
89
|
|
90
90
|
repo = Rugged::Repository.new(dir)
|
91
91
|
if repo.head_detached?
|
92
|
-
log VERBOSE, " Skipping #{short_dir} because it is in a detached HEAD state", :yellow
|
92
|
+
log Logging::VERBOSE, " Skipping #{short_dir} because it is in a detached HEAD state", :yellow
|
93
93
|
return
|
94
94
|
end
|
95
95
|
|
96
|
-
Timeout.timeout(
|
96
|
+
Timeout.timeout(walker.config.git_timeout) do
|
97
97
|
unless repo_has_changes?(dir)
|
98
|
-
log DEBUG, " No changes to commit in #{short_dir}", :green
|
98
|
+
log Logging::DEBUG, " No changes to commit in #{short_dir}", :green
|
99
99
|
return
|
100
100
|
end
|
101
101
|
commit_changes(dir, message, short_dir)
|
102
102
|
end
|
103
103
|
rescue Timeout::Error
|
104
|
-
log NORMAL, "[TIMEOUT] Thread #{thread_id}: git operations timed out in #{short_dir}", :red
|
104
|
+
log Logging::NORMAL, "[TIMEOUT] Thread #{thread_id}: git operations timed out in #{short_dir}", :red
|
105
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 }
|
106
|
+
log Logging::NORMAL, "#{e.class} processing #{short_dir}: #{e.message}", :red
|
107
|
+
e.backtrace.join("\n").each_line { |line| log Logging::DEBUG, line, :red }
|
108
108
|
end
|
109
109
|
end
|
110
110
|
|
111
|
+
# @return [Boolean] True if the repository has changes, false otherwise.
|
112
|
+
def repo_has_changes?(dir)
|
113
|
+
repo = Rugged::Repository.new(dir)
|
114
|
+
repo.status { |_file, status| return true if status != :current && status != :ignored }
|
115
|
+
false
|
116
|
+
end
|
117
|
+
|
111
118
|
# @param dir [String] The path to the git repository.
|
112
119
|
# @return [Boolean] True if the repository has changes, false otherwise.
|
113
120
|
def repo_has_staged_changes?(repo)
|
@@ -137,7 +144,7 @@ module GitTree
|
|
137
144
|
|
138
145
|
current_branch = repo.head.name.sub('refs/heads/', '')
|
139
146
|
system('git', '-C', dir, 'push', '--set-upstream', 'origin', current_branch, exception: true)
|
140
|
-
log NORMAL, "Committed and pushed changes in #{short_dir}", :green
|
147
|
+
log Logging::NORMAL, "Committed and pushed changes in #{short_dir}", :green
|
141
148
|
end
|
142
149
|
end
|
143
150
|
end
|
@@ -146,10 +153,10 @@ if $PROGRAM_NAME == __FILE__ || $PROGRAM_NAME.end_with?('git-commitAll') # Corre
|
|
146
153
|
begin
|
147
154
|
GitTree::CommitAllCommand.new(ARGV).run
|
148
155
|
rescue Interrupt
|
149
|
-
log NORMAL, "\nInterrupted by user", :yellow
|
156
|
+
log Logging::NORMAL, "\nInterrupted by user", :yellow
|
150
157
|
exit! 130 # Use exit! to prevent further exceptions on shutdown
|
151
158
|
rescue StandardError => e
|
152
|
-
log QUIET, "#{e.class}: #{e.message}\n#{e.backtrace.join("\n")}", :red
|
159
|
+
log Logging::QUIET, "#{e.class}: #{e.message}\n#{e.backtrace.join("\n")}", :red
|
153
160
|
exit 1
|
154
161
|
end
|
155
162
|
end
|
data/lib/commands/git_evars.rb
CHANGED
@@ -18,7 +18,7 @@ module GitTree
|
|
18
18
|
if @options[:zowee]
|
19
19
|
walker = GitTreeWalker.new(@args, options: @options)
|
20
20
|
all_paths = []
|
21
|
-
walker.find_and_process_repos do |dir,
|
21
|
+
walker.find_and_process_repos do |dir, _root_arg|
|
22
22
|
all_paths << dir
|
23
23
|
end
|
24
24
|
optimizer = ZoweeOptimizer.new(walker.root_map)
|
@@ -26,7 +26,7 @@ module GitTree
|
|
26
26
|
elsif @args.empty? # No args provided, use default roots and substitute them in the output
|
27
27
|
walker = GitTreeWalker.new([], options: @options)
|
28
28
|
walker.find_and_process_repos do |dir, _root_arg|
|
29
|
-
result << make_env_var_with_substitution(dir,
|
29
|
+
result << make_env_var_with_substitution(dir, walker.config.default_roots)
|
30
30
|
end
|
31
31
|
else # Args were provided, process them as roots
|
32
32
|
processed_args = @args.flat_map { |arg| arg.strip.split(/\s+/) }
|
@@ -47,12 +47,12 @@ module GitTree
|
|
47
47
|
# @param msg [String] The error message to display before the help text.
|
48
48
|
# @return [nil]
|
49
49
|
def help(msg = nil)
|
50
|
-
log(QUIET, "Error: #{msg}\n", :red) if msg
|
51
|
-
log QUIET, <<~END_HELP
|
50
|
+
log(Logging::QUIET, "Error: #{msg}\n", :red) if msg
|
51
|
+
log Logging::QUIET, <<~END_HELP
|
52
52
|
#{$PROGRAM_NAME} - Generate bash environment variables for each git repository found under specified directory trees.
|
53
53
|
|
54
54
|
Examines trees of git repositories and writes a bash script to STDOUT.
|
55
|
-
If no directories are given, uses default
|
55
|
+
If no directories are given, uses default roots (#{@config.default_roots.join(', ')}) as roots.
|
56
56
|
These environment variables point to roots of git repository trees to walk.
|
57
57
|
Skips directories containing a .ignore file, and all subdirectories.
|
58
58
|
|
@@ -150,10 +150,10 @@ if $PROGRAM_NAME == __FILE__ || $PROGRAM_NAME.end_with?('git-evars')
|
|
150
150
|
begin
|
151
151
|
GitTree::EvarsCommand.new(ARGV).run
|
152
152
|
rescue Interrupt
|
153
|
-
log NORMAL, "\nInterrupted by user", :yellow
|
153
|
+
log Logging::NORMAL, "\nInterrupted by user", :yellow
|
154
154
|
exit! 130 # Use exit! to prevent further exceptions on shutdown
|
155
155
|
rescue StandardError => e
|
156
|
-
log QUIET, "#{e.class}: #{e.message}\n#{e.backtrace.join("\n")}", :red
|
156
|
+
log Logging::QUIET, "#{e.class}: #{e.message}\n#{e.backtrace.join("\n")}", :red
|
157
157
|
exit! 1
|
158
158
|
end
|
159
159
|
end
|
data/lib/commands/git_exec.rb
CHANGED
@@ -35,6 +35,9 @@ module GitTree
|
|
35
35
|
def execute_and_log(dir, command)
|
36
36
|
output, status = @runner.run(command, dir)
|
37
37
|
log_result(output, status.success?)
|
38
|
+
rescue Errno::ENOENT
|
39
|
+
error_message = "Error: Command '#{command}' not found in #{dir}"
|
40
|
+
log_result(error_message, false)
|
38
41
|
rescue StandardError => e
|
39
42
|
error_message = "Error: '#{e.message}' from executing '#{command}' in #{dir}"
|
40
43
|
log_result(error_message, false)
|
@@ -48,16 +51,16 @@ module GitTree
|
|
48
51
|
log_stdout output.strip
|
49
52
|
else
|
50
53
|
# Errors should go to STDERR.
|
51
|
-
log QUIET, output.strip, :red
|
54
|
+
log Logging::QUIET, output.strip, :red
|
52
55
|
end
|
53
56
|
end
|
54
57
|
|
55
58
|
def help(msg = nil)
|
56
|
-
log(QUIET, "Error: #{msg}\n", :red) if msg
|
57
|
-
log QUIET, <<~END_HELP
|
59
|
+
log(Logging::QUIET, "Error: #{msg}\n", :red) if msg
|
60
|
+
log Logging::QUIET, <<~END_HELP
|
58
61
|
#{$PROGRAM_NAME} - Executes an arbitrary shell command for each repository.
|
59
62
|
|
60
|
-
If no arguments are given, uses default
|
63
|
+
If no arguments are given, uses default roots (#{@config.default_roots.join(', ')}) as roots.
|
61
64
|
These environment variables point to roots of git repository trees to walk.
|
62
65
|
Skips directories containing a .ignore file, and all subdirectories.
|
63
66
|
|
@@ -95,10 +98,10 @@ if $PROGRAM_NAME == __FILE__ || $PROGRAM_NAME.end_with?('git-exec')
|
|
95
98
|
begin
|
96
99
|
GitTree::ExecCommand.new(ARGV).run
|
97
100
|
rescue Interrupt
|
98
|
-
log NORMAL, "\nInterrupted by user", :yellow
|
101
|
+
log Logging::NORMAL, "\nInterrupted by user", :yellow
|
99
102
|
exit! 130 # Use exit! to prevent further exceptions on shutdown
|
100
103
|
rescue StandardError => e
|
101
|
-
log QUIET, "#{e.class}: #{e.message}\n#{e.backtrace.join("\n")}", :red
|
104
|
+
log Logging::QUIET, "#{e.class}: #{e.message}\n#{e.backtrace.join("\n")}", :red
|
102
105
|
exit 1
|
103
106
|
end
|
104
107
|
end
|
@@ -23,10 +23,10 @@ module GitTree
|
|
23
23
|
private
|
24
24
|
|
25
25
|
def help(msg = nil)
|
26
|
-
log(QUIET, "Error: #{msg}\n", :red) if msg
|
27
|
-
log QUIET, <<~END_HELP
|
26
|
+
log(Logging::QUIET, "Error: #{msg}\n", :red) if msg
|
27
|
+
log Logging::QUIET, <<~END_HELP
|
28
28
|
#{$PROGRAM_NAME} - Replicates trees of git repositories and writes a bash script to STDOUT.
|
29
|
-
If no directories are given, uses default
|
29
|
+
If no directories are given, uses default roots (#{@config.default_roots.join(', ')}) as roots.
|
30
30
|
The script clones the repositories and replicates any remotes.
|
31
31
|
Skips directories containing a .ignore file.
|
32
32
|
|
@@ -82,10 +82,10 @@ if $PROGRAM_NAME == __FILE__ || $PROGRAM_NAME.end_with?('git-replicate') # Corre
|
|
82
82
|
begin
|
83
83
|
GitTree::ReplicateCommand.new(ARGV).run
|
84
84
|
rescue Interrupt
|
85
|
-
log NORMAL, "\nInterrupted by user", :yellow
|
85
|
+
log Logging::NORMAL, "\nInterrupted by user", :yellow
|
86
86
|
exit! 130 # Use exit! to prevent further exceptions on shutdown
|
87
87
|
rescue StandardError => e
|
88
|
-
log QUIET, "#{e.class}: #{e.message}\n#{e.backtrace.join("\n")}", :red
|
88
|
+
log Logging::QUIET, "#{e.class}: #{e.message}\n#{e.backtrace.join("\n")}", :red
|
89
89
|
exit! 1
|
90
90
|
end
|
91
91
|
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'highline'
|
2
|
+
require 'yaml'
|
3
|
+
require_relative '../util/config'
|
4
|
+
require_relative '../util/log'
|
5
|
+
|
6
|
+
module GitTree
|
7
|
+
# A command to interactively create a user-level configuration file.
|
8
|
+
class TreeconfigCommand
|
9
|
+
include Logging
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
$PROGRAM_NAME = 'git-treeconfig'
|
13
|
+
@cli = HighLine.new
|
14
|
+
@config_path = GitTree::Config.default_config_path
|
15
|
+
@existing_config = File.exist?(@config_path) ? YAML.load_file(@config_path) : {}
|
16
|
+
end
|
17
|
+
|
18
|
+
def run
|
19
|
+
@cli.say "Welcome to git-tree configuration."
|
20
|
+
@cli.say "This utility will help you create a configuration file at: #{@config_path}"
|
21
|
+
@cli.say "Press Enter to accept the default value in brackets."
|
22
|
+
@cli.say ""
|
23
|
+
|
24
|
+
defaults = GitTree::Config.new
|
25
|
+
|
26
|
+
new_config = {}
|
27
|
+
new_config['git_timeout'] = @cli.ask("Git command timeout in seconds? ", Integer) do |q|
|
28
|
+
q.default = @existing_config.fetch('git_timeout', defaults.git_timeout)
|
29
|
+
end
|
30
|
+
|
31
|
+
new_config['verbosity'] = @cli.ask("Default verbosity level (0=quiet, 1=normal, 2=verbose)? ", Integer) do |q|
|
32
|
+
q.default = @existing_config.fetch('verbosity', defaults.verbosity)
|
33
|
+
q.in = 0..2
|
34
|
+
end
|
35
|
+
|
36
|
+
roots_str = @cli.ask("Default root directories (space-separated)? ", String) do |q|
|
37
|
+
q.default = @existing_config.fetch('default_roots', defaults.default_roots).join(' ')
|
38
|
+
end
|
39
|
+
new_config['default_roots'] = roots_str.split
|
40
|
+
|
41
|
+
File.write(@config_path, new_config.to_yaml)
|
42
|
+
|
43
|
+
@cli.say ""
|
44
|
+
@cli.say @cli.color("Configuration saved to #{@config_path}", :green)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
if $PROGRAM_NAME == __FILE__ || $PROGRAM_NAME.end_with?('git-treeconfig')
|
50
|
+
begin
|
51
|
+
GitTree::TreeconfigCommand.new.run
|
52
|
+
rescue Interrupt
|
53
|
+
# Using HighLine, a simple newline is enough on interrupt.
|
54
|
+
puts "\n"
|
55
|
+
exit 130
|
56
|
+
rescue StandardError => e
|
57
|
+
warn "An error occurred: #{e.message}" # Cannot use `log` here as it's outside the class scope
|
58
|
+
exit 1
|
59
|
+
end
|
60
|
+
end
|
data/lib/commands/git_update.rb
CHANGED
@@ -32,11 +32,11 @@ module GitTree
|
|
32
32
|
private
|
33
33
|
|
34
34
|
def help(msg = nil)
|
35
|
-
log(QUIET, "Error: #{msg}\n", :red) if msg
|
36
|
-
log QUIET, <<~END_HELP
|
35
|
+
log(Logging::QUIET, "Error: #{msg}\n", :red) if msg
|
36
|
+
log Logging::QUIET, <<~END_HELP
|
37
37
|
git-update - Recursively updates trees of git repositories.
|
38
38
|
|
39
|
-
If no arguments are given, uses default
|
39
|
+
If no arguments are given, uses default roots (#{@config.default_roots.join(', ')}) as roots.
|
40
40
|
These environment variables point to roots of git repository trees to walk.
|
41
41
|
Skips directories containing a .ignore file, and all subdirectories.
|
42
42
|
|
@@ -70,31 +70,31 @@ module GitTree
|
|
70
70
|
# @return [nil]
|
71
71
|
def process_repo(git_walker, dir, thread_id)
|
72
72
|
abbrev_dir = git_walker.abbreviate_path(dir)
|
73
|
-
log NORMAL, "Updating #{abbrev_dir}", :green
|
74
|
-
log VERBOSE, "Thread #{thread_id}: git -C #{dir} pull", :yellow
|
73
|
+
log Logging::NORMAL, "Updating #{abbrev_dir}", :green
|
74
|
+
log Logging::VERBOSE, "Thread #{thread_id}: git -C #{dir} pull", :yellow
|
75
75
|
|
76
76
|
output = nil
|
77
77
|
status = nil
|
78
78
|
begin
|
79
|
-
Timeout.timeout(
|
80
|
-
log VERBOSE, "Executing: git pull in #{dir}", :yellow
|
79
|
+
Timeout.timeout(git_walker.config.git_timeout) do
|
80
|
+
log Logging::VERBOSE, "Executing: git pull in #{dir}", :yellow
|
81
81
|
output, status_obj = @runner.run('git pull', dir)
|
82
82
|
status = status_obj.exitstatus
|
83
83
|
end
|
84
84
|
rescue Timeout::Error
|
85
|
-
log NORMAL, "[TIMEOUT] Thread #{thread_id}: git pull timed out in #{abbrev_dir}", :red
|
85
|
+
log Logging::NORMAL, "[TIMEOUT] Thread #{thread_id}: git pull timed out in #{abbrev_dir}", :red
|
86
86
|
status = -1
|
87
87
|
rescue StandardError => e
|
88
|
-
log NORMAL, "[ERROR] Thread #{thread_id}: #{e.class} in #{abbrev_dir}; #{e.message}\n#{e.backtrace.join("\n")}", :red
|
88
|
+
log Logging::NORMAL, "[ERROR] Thread #{thread_id}: #{e.class} in #{abbrev_dir}; #{e.message}\n#{e.backtrace.join("\n")}", :red
|
89
89
|
status = -1
|
90
90
|
end
|
91
91
|
|
92
92
|
if !status.zero?
|
93
|
-
log NORMAL, "[ERROR] git pull failed in #{abbrev_dir} (exit code #{status}):", :red
|
94
|
-
log NORMAL, output.strip, :red unless output.to_s.strip.empty?
|
95
|
-
elsif Logging.verbosity >= VERBOSE
|
93
|
+
log Logging::NORMAL, "[ERROR] git pull failed in #{abbrev_dir} (exit code #{status}):", :red
|
94
|
+
log Logging::NORMAL, output.strip, :red unless output.to_s.strip.empty?
|
95
|
+
elsif Logging.verbosity >= Logging::VERBOSE
|
96
96
|
# Output from a successful pull is considered NORMAL level
|
97
|
-
log NORMAL, output.strip, :green
|
97
|
+
log Logging::NORMAL, output.strip, :green
|
98
98
|
end
|
99
99
|
end
|
100
100
|
end
|
@@ -104,10 +104,10 @@ if $PROGRAM_NAME == __FILE__ || $PROGRAM_NAME.end_with?('git-update')
|
|
104
104
|
begin
|
105
105
|
GitTree::UpdateCommand.new(ARGV).run
|
106
106
|
rescue Interrupt
|
107
|
-
log NORMAL, "\nInterrupted by user", :yellow
|
107
|
+
log Logging::NORMAL, "\nInterrupted by user", :yellow
|
108
108
|
exit! 130
|
109
109
|
rescue StandardError => e
|
110
|
-
log QUIET, "#{e.class}: #{e.message}\n#{e.backtrace.join("\n")}", :red
|
110
|
+
log Logging::QUIET, "#{e.class}: #{e.message}\n#{e.backtrace.join("\n")}", :red
|
111
111
|
exit! 1
|
112
112
|
end
|
113
113
|
end
|
data/lib/git_tree/version.rb
CHANGED
data/lib/util/config.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'anyway'
|
2
|
+
require_relative 'log'
|
3
|
+
|
4
|
+
module GitTree
|
5
|
+
# Centralized configuration for the git-tree gem.
|
6
|
+
# Uses anyway_config to load settings from:
|
7
|
+
# 1. A YAML file at ~/.treeconfig.yml
|
8
|
+
# 2. Environment variables (e.g., GIT_TREE_GIT_TIMEOUT)
|
9
|
+
# 3. Default values defined here.
|
10
|
+
class Config < Anyway::Config
|
11
|
+
config_name :treeconfig
|
12
|
+
env_prefix 'GIT_TREE'
|
13
|
+
|
14
|
+
# The location of the user's config file.
|
15
|
+
def self.default_config_path
|
16
|
+
File.expand_path('~/.treeconfig.yml')
|
17
|
+
end
|
18
|
+
|
19
|
+
# Define attributes with their default values.
|
20
|
+
attr_config :git_timeout, :verbosity, :default_roots
|
21
|
+
|
22
|
+
# Override initialize to set defaults for nil values after loading.
|
23
|
+
def initialize(*)
|
24
|
+
super
|
25
|
+
self.git_timeout ||= 300
|
26
|
+
self.verbosity ||= ::Logging::NORMAL
|
27
|
+
self.default_roots ||= %w[sites sitesUbuntu work]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/util/git_tree_walker.rb
CHANGED
@@ -2,17 +2,16 @@ require 'English'
|
|
2
2
|
require 'etc'
|
3
3
|
require 'shellwords'
|
4
4
|
require 'optparse'
|
5
|
-
require 'timeout'
|
6
|
-
require_relative '
|
5
|
+
require 'timeout' # This is correct, no change needed here.
|
6
|
+
require_relative 'config'
|
7
7
|
require_relative 'log'
|
8
|
+
require_relative 'thread_pool_manager'
|
8
9
|
|
9
10
|
class GitTreeWalker
|
10
11
|
include Logging
|
11
12
|
|
12
|
-
attr_reader :display_roots, :root_map
|
13
|
+
attr_reader :config, :display_roots, :root_map
|
13
14
|
|
14
|
-
DEFAULT_ROOTS = %w[sites sitesUbuntu work].freeze
|
15
|
-
GIT_TIMEOUT = 10 # TODO: for debuggin only; should be 300 # 5 minutes per git pull
|
16
15
|
IGNORED_DIRECTORIES = ['.', '..', '.venv'].freeze
|
17
16
|
|
18
17
|
def initialize(args = ARGV, options: {})
|
@@ -20,6 +19,8 @@ class GitTreeWalker
|
|
20
19
|
@root_map = {}
|
21
20
|
@display_roots = []
|
22
21
|
determine_roots(args)
|
22
|
+
@config = GitTree::Config.new
|
23
|
+
log Logging::VERBOSE, "GitTreeWalker#initialize: verbosity is #{Logging.verbosity}"
|
23
24
|
end
|
24
25
|
|
25
26
|
def abbreviate_path(dir)
|
@@ -32,9 +33,9 @@ class GitTreeWalker
|
|
32
33
|
end
|
33
34
|
|
34
35
|
def process(&) # Accepts a block
|
35
|
-
log VERBOSE, "Processing #{@display_roots.join(' ')}", :green
|
36
|
+
log Logging::VERBOSE, "Processing #{@display_roots.join(' ')}", :green
|
36
37
|
if @options[:serial]
|
37
|
-
log VERBOSE, "Running in serial mode.", :yellow
|
38
|
+
log Logging::VERBOSE, "Running in serial mode.", :yellow
|
38
39
|
find_and_process_repos do |dir, _root_arg|
|
39
40
|
yield(dir, 0, self) # task, thread_id, walker
|
40
41
|
end
|
data/lib/util/log.rb
CHANGED
@@ -4,13 +4,14 @@ module Logging
|
|
4
4
|
using Rainbow
|
5
5
|
|
6
6
|
# Verbosity levels
|
7
|
-
QUIET
|
8
|
-
NORMAL
|
7
|
+
QUIET = 0
|
8
|
+
NORMAL = 1
|
9
9
|
VERBOSE = 2
|
10
|
-
DEBUG
|
10
|
+
DEBUG = 3
|
11
11
|
|
12
12
|
# Class-level instance variables to hold the verbosity setting for the module
|
13
|
-
@verbosity = NORMAL
|
13
|
+
@verbosity = ::Logging::NORMAL
|
14
|
+
# warn "Logging module loaded. Default verbosity: #{@verbosity}" if @verbosity >= NORMAL
|
14
15
|
|
15
16
|
# @return [Integer] The current verbosity level.
|
16
17
|
def self.verbosity
|
@@ -20,6 +21,9 @@ module Logging
|
|
20
21
|
# @param level [Integer] The new verbosity level.
|
21
22
|
# @return [nil]
|
22
23
|
def self.verbosity=(level)
|
24
|
+
# warn "Logging.verbosity= called. Changing from #{@verbosity} to #{level}" \
|
25
|
+
# if (@verbosity || NORMAL) >= NORMAL ||
|
26
|
+
# (level || NORMAL) >= NORMAL
|
23
27
|
@verbosity = level
|
24
28
|
end
|
25
29
|
|
@@ -10,12 +10,13 @@ class FixedThreadPoolManager
|
|
10
10
|
# (less one for the monitor thread), with a minimum of 1.
|
11
11
|
# @param percent_available_processors [Float] The percentage of available processors to use for worker threads.
|
12
12
|
def initialize(percent_available_processors = 0.75)
|
13
|
+
log Logging::VERBOSE, "FixedThreadPoolManager#initialize: verbosity is #{Logging.verbosity}"
|
13
14
|
if percent_available_processors > 1 || percent_available_processors <= 0
|
14
15
|
msg = <<~END_MSG
|
15
16
|
Error: The allowable range for the ThreadPool.initialize percent_available_processors is between 0 and 1.
|
16
17
|
You provided #{percent_available_processors}.
|
17
18
|
END_MSG
|
18
|
-
log QUIET, msg, :red
|
19
|
+
log Logging::QUIET, msg, :red
|
19
20
|
exit! 1
|
20
21
|
end
|
21
22
|
@worker_count = [(Etc.nprocessors * percent_available_processors).floor, 1].max
|
@@ -61,7 +62,7 @@ class FixedThreadPoolManager
|
|
61
62
|
break if active_workers.zero?
|
62
63
|
|
63
64
|
if active_workers != last_active_count
|
64
|
-
warn format("Waiting for %d worker threads to complete...", active_workers) + "\r" if Logging.verbosity > NORMAL
|
65
|
+
warn format("Waiting for %d worker threads to complete...", active_workers) + "\r" if Logging.verbosity > ::Logging::NORMAL
|
65
66
|
last_active_count = active_workers
|
66
67
|
end
|
67
68
|
begin
|
@@ -73,16 +74,16 @@ class FixedThreadPoolManager
|
|
73
74
|
end
|
74
75
|
|
75
76
|
warn (" " * 60) + "\r" # Clear the line
|
76
|
-
log
|
77
|
+
log Logging::VERBOSE, "All work is complete.", :green
|
77
78
|
end
|
78
79
|
|
79
80
|
private
|
80
81
|
|
81
82
|
def initialize_workers
|
82
|
-
log
|
83
|
+
log Logging::DEBUG, "Initializing #{@worker_count} worker threads...", :green
|
83
84
|
@worker_count.times do |i|
|
84
85
|
worker_thread = Thread.new do
|
85
|
-
log
|
86
|
+
log Logging::DEBUG, " [Worker #{i}] Started.", :cyan
|
86
87
|
start_time = Time.now
|
87
88
|
start_cpu = Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID)
|
88
89
|
tasks_processed = 0
|
@@ -95,14 +96,14 @@ class FixedThreadPoolManager
|
|
95
96
|
tasks_processed += 1
|
96
97
|
end
|
97
98
|
|
98
|
-
if Logging.verbosity >= VERBOSE
|
99
|
+
if Logging.verbosity >= ::Logging::VERBOSE
|
99
100
|
elapsed_time = Time.now - start_time
|
100
101
|
cpu_time = Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID) - start_cpu
|
101
102
|
shutdown_msg = format(
|
102
103
|
" [Worker #{i}] Shutting down. Processed #{tasks_processed} tasks. Elapsed: %.2fs, CPU: %.2fs",
|
103
104
|
elapsed_time, cpu_time
|
104
105
|
)
|
105
|
-
log VERBOSE, shutdown_msg, :cyan
|
106
|
+
log Logging::VERBOSE, shutdown_msg, :cyan
|
106
107
|
end
|
107
108
|
rescue Interrupt
|
108
109
|
# This thread was interrupted by Ctrl-C, likely while waiting on the queue.
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: git_tree
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mike Slinn
|
@@ -9,6 +9,34 @@ bindir: exe
|
|
9
9
|
cert_chain: []
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: anyway_config
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - "~>"
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '2.0'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - "~>"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '2.0'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: highline
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
12
40
|
- !ruby/object:Gem::Dependency
|
13
41
|
name: rainbow
|
14
42
|
requirement: !ruby/object:Gem::Requirement
|
@@ -58,6 +86,7 @@ executables:
|
|
58
86
|
- git-evars
|
59
87
|
- git-exec
|
60
88
|
- git-replicate
|
89
|
+
- git-treeconfig
|
61
90
|
- git-update
|
62
91
|
extensions: []
|
63
92
|
extra_rdoc_files: []
|
@@ -71,6 +100,7 @@ files:
|
|
71
100
|
- exe/git-evars
|
72
101
|
- exe/git-exec
|
73
102
|
- exe/git-replicate
|
103
|
+
- exe/git-treeconfig
|
74
104
|
- exe/git-update
|
75
105
|
- git_tree.gemspec
|
76
106
|
- lib/commands/abstract_command.rb
|
@@ -79,10 +109,12 @@ files:
|
|
79
109
|
- lib/commands/git_exec.rb
|
80
110
|
- lib/commands/git_exec_spec.rb
|
81
111
|
- lib/commands/git_replicate.rb
|
112
|
+
- lib/commands/git_treeconfig.rb
|
82
113
|
- lib/commands/git_update.rb
|
83
114
|
- lib/git_tree.rb
|
84
115
|
- lib/git_tree/version.rb
|
85
116
|
- lib/util/command_runner.rb
|
117
|
+
- lib/util/config.rb
|
86
118
|
- lib/util/gem_support.rb
|
87
119
|
- lib/util/git_tree_walker.rb
|
88
120
|
- lib/util/git_tree_walker_private.rb
|
@@ -102,6 +134,10 @@ post_install_message: |2+
|
|
102
134
|
|
103
135
|
Thanks for installing git_tree!
|
104
136
|
|
137
|
+
To customize default settings like timeout and repository roots,
|
138
|
+
run the interactive configuration tool:
|
139
|
+
git-treeconfig
|
140
|
+
|
105
141
|
rdoc_options: []
|
106
142
|
require_paths:
|
107
143
|
- lib
|