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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3592f1261444da8733168030eebf398f6055fb5f53e2c5758b44fac69e9092fe
4
- data.tar.gz: caae86a90d3acaf2267b58de6449625fbf23a010756539ee0fd9b65efe6a6678
3
+ metadata.gz: 4703e322963fd70470c26e91b28bbc1bee8928abe0b8090ba7eabc5d1970f1d5
4
+ data.tar.gz: 2fd2b50f103e7b6ea25d917337f1710e31f5ed01c963a5eb07b1917f32fb4a00
5
5
  SHA512:
6
- metadata.gz: b02edfd02bbb8589f53bdafc76b4a298b8fff75fdeee4af016b7912e85220c35366bee1e83cf1a95deb923202cdb687c32e80610dccd8408b8918a5f4b09de6e
7
- data.tar.gz: 9281499cf2057b365a6ac8e01de640fe457d7d1e463c7e10da8e25c203ff3917f3673333de618db645a70c5c1da47eb4058a4922b925e317ab2cbc3ebbc639db
6
+ metadata.gz: 3f6f72f9f8d29388eb116423b34236fc876e3b14de99f2024cec036e562afc9be2bde0973ef78d80728e420d4b5ceedcef3906c0993c156362d89939ee674214
7
+ data.tar.gz: bb56c09a095f234c0c3b8a9373fe4e8c5cdd13b3935a28a7af6c2b1c671aa94c365b5f5dfacaaf15d0e1a8e7f2e9a09dc18e6c59c8c6a7913c491e30cffa4f8c
data/.rubocop.yml CHANGED
@@ -79,6 +79,9 @@ Naming/FileName:
79
79
  RSpec/ExampleLength:
80
80
  Max: 20
81
81
 
82
+ RSpec/InstanceVariable:
83
+ Enabled: false
84
+
82
85
  RSpec/MultipleExpectations:
83
86
  Max: 15
84
87
 
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,
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/commands/git_treeconfig'
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/git_tree_walker'
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
- @raw_args = args
15
- @options = { serial: false }.merge(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
- # 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.
24
+ # Common setup for all commands.
25
+ # Parses options and sets initial verbosity.
21
26
  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."
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
- # 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.
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.") do
38
+ opts.on("-h", "--help", "Show this help message and exit") do
43
39
  help
44
40
  end
45
- opts.on("-q", "--quiet", "Suppress normal output, only show errors.") do
46
- parsed_options[:verbosity] = QUIET
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
- 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
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
- opts.on('-s', "--serial", "Run tasks serially in a single thread in the order specified.") do
54
- parsed_options[:serial] = true
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
- # 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
57
+ yield(opts) if block_given?
70
58
  end
71
59
 
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
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 environment variables (#{GitTreeWalker::DEFAULT_ROOTS.join(', ')}) as roots.
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(GitTreeWalker::GIT_TIMEOUT) do
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
@@ -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, GitTreeWalker::DEFAULT_ROOTS)
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 environment variables (#{GitTreeWalker::DEFAULT_ROOTS.join(', ')}) as roots.
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
@@ -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 environment variables (#{GitTreeWalker::DEFAULT_ROOTS.join(', ')}) as roots.
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 environment variables (#{GitTreeWalker::DEFAULT_ROOTS.join(', ')}) as roots.
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
@@ -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 environment variables (#{GitTreeWalker::DEFAULT_ROOTS.join(', ')}) as roots.
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(GitTreeWalker::GIT_TIMEOUT) do
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
@@ -1,3 +1,3 @@
1
1
  module GitUrlsVersion
2
- VERSION = '1.0.1'.freeze
2
+ VERSION = '1.0.2'.freeze
3
3
  end
@@ -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
@@ -2,17 +2,16 @@ require 'English'
2
2
  require 'etc'
3
3
  require 'shellwords'
4
4
  require 'optparse'
5
- require 'timeout'
6
- require_relative 'thread_pool_manager'
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 = 0
8
- NORMAL = 1
7
+ QUIET = 0
8
+ NORMAL = 1
9
9
  VERBOSE = 2
10
- DEBUG = 3
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 NORMAL, "All work is complete.", :green
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 NORMAL, "Initializing #{@worker_count} worker threads...", :green
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 NORMAL, " [Worker #{i}] Started.", :cyan
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.1
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