sxn 0.2.2 โ†’ 0.2.5

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.
@@ -14,7 +14,7 @@ module Sxn
14
14
  @project_manager = ProjectManager.new(@config_manager)
15
15
  end
16
16
 
17
- def add_worktree(project_name, branch = nil, session_name: nil)
17
+ def add_worktree(project_name, branch = nil, session_name: nil, verbose: false)
18
18
  # Use current session if not specified
19
19
  session_name ||= @config_manager.current_session
20
20
  raise Sxn::NoActiveSessionError, "No active session. Use 'sxn use <session>' first." unless session_name
@@ -25,8 +25,32 @@ module Sxn
25
25
  project = @project_manager.get_project(project_name)
26
26
  raise Sxn::ProjectNotFoundError, "Project '#{project_name}' not found" unless project
27
27
 
28
- # Use default branch if not specified
29
- branch ||= project[:default_branch] || "master"
28
+ # Determine branch name
29
+ # If no branch specified, use session's default branch from .sxnrc, then fallback to session name
30
+ # If branch starts with "remote:", handle remote branch tracking
31
+ if branch.nil?
32
+ session_config = SessionConfig.new(session[:path])
33
+ branch = session_config.default_branch if session_config.exists?
34
+ branch ||= session_name
35
+ elsif branch.start_with?("remote:")
36
+ remote_branch = branch.sub("remote:", "")
37
+ # Fetch the remote branch first
38
+ begin
39
+ fetch_remote_branch(project[:path], remote_branch)
40
+ branch = remote_branch
41
+ rescue StandardError => e
42
+ raise Sxn::WorktreeCreationError, "Failed to fetch remote branch: #{e.message}"
43
+ end
44
+ end
45
+
46
+ if ENV["SXN_DEBUG"]
47
+ puts "[DEBUG] Adding worktree:"
48
+ puts " Project: #{project_name}"
49
+ puts " Project path: #{project[:path]}"
50
+ puts " Session: #{session_name}"
51
+ puts " Session path: #{session[:path]}"
52
+ puts " Branch: #{branch}"
53
+ end
30
54
 
31
55
  # Check if worktree already exists in this session
32
56
  existing_worktrees = @session_manager.get_session_worktrees(session_name)
@@ -38,9 +62,14 @@ module Sxn
38
62
  # Create worktree path
39
63
  worktree_path = File.join(session[:path], project_name)
40
64
 
65
+ puts " Worktree path: #{worktree_path}" if ENV["SXN_DEBUG"]
66
+
41
67
  begin
68
+ # Handle orphaned worktree if it exists
69
+ handle_orphaned_worktree(project[:path], worktree_path)
70
+
42
71
  # Create the worktree
43
- create_git_worktree(project[:path], worktree_path, branch)
72
+ create_git_worktree(project[:path], worktree_path, branch, verbose: verbose)
44
73
 
45
74
  # Register worktree with session
46
75
  @session_manager.add_worktree_to_session(session_name, project_name, worktree_path, branch)
@@ -54,6 +83,11 @@ module Sxn
54
83
  rescue StandardError => e
55
84
  # Clean up on failure
56
85
  FileUtils.rm_rf(worktree_path)
86
+
87
+ # If it's already our error with details, re-raise it
88
+ raise e if e.is_a?(Sxn::WorktreeCreationError)
89
+
90
+ # Otherwise wrap it
57
91
  raise Sxn::WorktreeCreationError, "Failed to create worktree: #{e.message}"
58
92
  end
59
93
  end
@@ -169,7 +203,48 @@ module Sxn
169
203
 
170
204
  private
171
205
 
172
- def create_git_worktree(project_path, worktree_path, branch)
206
+ def handle_orphaned_worktree(project_path, worktree_path)
207
+ Dir.chdir(project_path) do
208
+ # Try to prune worktrees first
209
+ system("git worktree prune", out: File::NULL, err: File::NULL)
210
+
211
+ # Check if this worktree is registered (whether it exists or not)
212
+ output = `git worktree list --porcelain 2>/dev/null`
213
+ if output.include?(worktree_path)
214
+ # Force remove the orphaned/existing worktree
215
+ system("git worktree remove --force #{Shellwords.escape(worktree_path)}", out: File::NULL, err: File::NULL)
216
+ end
217
+ end
218
+
219
+ # Remove the directory if it still exists
220
+ FileUtils.rm_rf(worktree_path)
221
+ end
222
+
223
+ def fetch_remote_branch(project_path, branch_name)
224
+ Dir.chdir(project_path) do
225
+ # First, fetch all remotes to ensure we have the latest branches
226
+ raise "Failed to fetch remote branches" unless system("git fetch --all", out: File::NULL, err: File::NULL)
227
+
228
+ # Check if the branch exists on any remote
229
+ remotes = `git remote`.lines.map(&:strip)
230
+ branch_found = false
231
+
232
+ remotes.each do |remote|
233
+ next unless system("git show-ref --verify --quiet refs/remotes/#{remote}/#{Shellwords.escape(branch_name)}",
234
+ out: File::NULL, err: File::NULL)
235
+
236
+ branch_found = true
237
+ # Set up tracking for the remote branch
238
+ system("git branch --track #{Shellwords.escape(branch_name)} #{remote}/#{Shellwords.escape(branch_name)}",
239
+ out: File::NULL, err: File::NULL)
240
+ break
241
+ end
242
+
243
+ raise "Remote branch '#{branch_name}' not found on any remote" unless branch_found
244
+ end
245
+ end
246
+
247
+ def create_git_worktree(project_path, worktree_path, branch, verbose: false)
173
248
  Dir.chdir(project_path) do
174
249
  # Check if branch exists
175
250
  branch_exists = system("git show-ref --verify --quiet refs/heads/#{Shellwords.escape(branch)}",
@@ -183,8 +258,59 @@ module Sxn
183
258
  ["git", "worktree", "add", "-b", branch, worktree_path]
184
259
  end
185
260
 
186
- success = system(*cmd, out: File::NULL, err: File::NULL)
187
- raise "Git worktree command failed" unless success
261
+ # Capture stderr for better error messages
262
+ require "open3"
263
+ stdout, stderr, status = Open3.capture3(*cmd)
264
+
265
+ unless status.success?
266
+ error_msg = stderr.empty? ? stdout : stderr
267
+ error_msg = "Git worktree command failed" if error_msg.strip.empty?
268
+
269
+ # Add more context to common errors
270
+ if error_msg.include?("already exists")
271
+ error_msg += "\nTry removing the existing worktree first with: sxn worktree remove #{File.basename(worktree_path)}"
272
+ elsif error_msg.include?("is already checked out")
273
+ error_msg += "\nThis branch is already checked out in another worktree"
274
+ elsif error_msg.include?("not a git repository")
275
+ error_msg = "Project '#{File.basename(project_path)}' is not a git repository"
276
+ elsif error_msg.include?("fatal: invalid reference")
277
+ # This typically means the branch doesn't exist and we're trying to create from a non-existent base
278
+ error_msg += "\nMake sure the repository has at least one commit or specify an existing branch"
279
+ elsif error_msg.include?("fatal:")
280
+ # Extract just the fatal error message for cleaner output
281
+ error_msg = error_msg.lines.grep(/fatal:/).first&.strip || error_msg
282
+ end
283
+
284
+ details = []
285
+ details << "Command: #{cmd.join(" ")}"
286
+ details << "Working directory: #{project_path}"
287
+ details << "Target path: #{worktree_path}"
288
+ details << "Branch: #{branch}"
289
+ details << ""
290
+ details << "Git output:"
291
+ details << "STDOUT: #{stdout.strip.empty? ? "(empty)" : stdout}"
292
+ details << "STDERR: #{stderr.strip.empty? ? "(empty)" : stderr}"
293
+ details << "Exit status: #{status.exitstatus}"
294
+
295
+ # Check for common issues
296
+ if !File.directory?(project_path)
297
+ details << "\nโš ๏ธ Project directory does not exist: #{project_path}"
298
+ elsif !File.directory?(File.join(project_path, ".git"))
299
+ details << "\nโš ๏ธ Not a git repository: #{project_path}"
300
+ details << " This might be a git submodule. Try:"
301
+ details << " 1. Ensure the project path points to the submodule directory"
302
+ details << " 2. Check if 'git submodule update --init' has been run"
303
+ end
304
+
305
+ details << "\nโš ๏ธ Target path already exists: #{worktree_path}" if File.exist?(worktree_path)
306
+
307
+ if ENV["SXN_DEBUG"] || verbose
308
+ puts "[DEBUG] Git worktree command failed:"
309
+ details.each { |line| puts " #{line}" }
310
+ end
311
+
312
+ raise Sxn::WorktreeCreationError.new(error_msg, details: details.join("\n"))
313
+ end
188
314
  end
189
315
  end
190
316
 
data/lib/sxn/core.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "core/config_manager"
4
+ require_relative "core/session_config"
4
5
  require_relative "core/session_manager"
5
6
  require_relative "core/project_manager"
6
7
  require_relative "core/worktree_manager"
data/lib/sxn/errors.rb CHANGED
@@ -37,7 +37,16 @@ module Sxn
37
37
  class WorktreeError < GitError; end
38
38
  class WorktreeExistsError < WorktreeError; end
39
39
  class WorktreeNotFoundError < WorktreeError; end
40
- class WorktreeCreationError < WorktreeError; end
40
+
41
+ class WorktreeCreationError < WorktreeError
42
+ attr_reader :details
43
+
44
+ def initialize(message, details: nil)
45
+ super(message)
46
+ @details = details
47
+ end
48
+ end
49
+
41
50
  class WorktreeRemovalError < WorktreeError; end
42
51
  class BranchError < GitError; end
43
52
 
data/lib/sxn/ui/prompt.rb CHANGED
@@ -68,6 +68,10 @@ module Sxn
68
68
  end
69
69
  end
70
70
 
71
+ def default_branch(session_name:)
72
+ branch_name("Default branch for worktrees:", default: session_name)
73
+ end
74
+
71
75
  def confirm_deletion(item_name, item_type = "item")
72
76
  ask_yes_no("Are you sure you want to delete #{item_type} '#{item_name}'? This action cannot be undone.",
73
77
  default: false)
data/lib/sxn/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sxn
4
- VERSION = "0.2.2"
4
+ VERSION = "0.2.5"
5
5
  end
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Setup git hooks for sxn development
4
+ #
5
+
6
+ set -e
7
+
8
+ echo "๐Ÿ”ง Setting up git hooks for sxn development..."
9
+
10
+ # Create hooks directory if it doesn't exist
11
+ mkdir -p .git/hooks
12
+
13
+ # Create pre-push hook
14
+ cat > .git/hooks/pre-push << 'EOF'
15
+ #!/bin/sh
16
+ #
17
+ # Pre-push hook for sxn project
18
+ # Runs RuboCop and RSpec before allowing push
19
+ #
20
+
21
+ echo "๐Ÿ” Running pre-push checks..."
22
+
23
+ # Run RuboCop
24
+ echo "๐Ÿ“ Checking code style with RuboCop..."
25
+ bundle exec rubocop
26
+ if [ $? -ne 0 ]; then
27
+ echo "โŒ RuboCop failed! Please fix linting issues before pushing."
28
+ echo " Run: bundle exec rubocop -a"
29
+ exit 1
30
+ fi
31
+
32
+ # Run RSpec tests
33
+ echo "๐Ÿงช Running tests with RSpec..."
34
+ bundle exec rspec --format progress
35
+ if [ $? -ne 0 ]; then
36
+ echo "โŒ Tests failed! Please fix failing tests before pushing."
37
+ exit 1
38
+ fi
39
+
40
+ echo "โœ… All checks passed! Proceeding with push..."
41
+ exit 0
42
+ EOF
43
+
44
+ chmod +x .git/hooks/pre-push
45
+
46
+ echo "โœ… Git hooks installed successfully!"
47
+ echo ""
48
+ echo "The following hooks have been set up:"
49
+ echo " โ€ข pre-push: Runs RuboCop and RSpec before pushing"
50
+ echo ""
51
+ echo "To bypass hooks in emergency (not recommended):"
52
+ echo " git push --no-verify"
data/sxn.gemspec ADDED
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/sxn/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "sxn"
7
+ spec.version = Sxn::VERSION
8
+ spec.authors = ["Ernest Sim"]
9
+ spec.email = ["ernest.codes@gmail.com"]
10
+
11
+ spec.summary = "Session management for multi-repository development"
12
+ spec.description = "Sxn simplifies git worktree management with intelligent project rules and secure automation"
13
+ spec.homepage = "https://github.com/idl3/sxn"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 3.2.0"
16
+
17
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
18
+ spec.metadata["homepage_uri"] = spec.homepage
19
+ spec.metadata["source_code_uri"] = spec.homepage
20
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
21
+ spec.metadata["rubygems_mfa_required"] = "true"
22
+
23
+ # Specify which files should be added to the gem when it is released.
24
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
25
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
26
+ `git ls-files -z`.split("\x0").reject do |f|
27
+ (f == __FILE__) ||
28
+ f.match(%r{\A(?:(?:test|spec|features)/|\.(?:git|travis|circleci)|appveyor)}) ||
29
+ f.match(/\.db-(?:shm|wal)\z/) || # Exclude SQLite temp files
30
+ f.match(/\.gem\z/) # Exclude gem files
31
+ end
32
+ end
33
+
34
+ spec.bindir = "bin"
35
+ spec.executables = spec.files.grep(%r{\Abin/}) { |f| File.basename(f) }
36
+ spec.require_paths = ["lib"]
37
+
38
+ # Core CLI dependencies
39
+ spec.add_dependency "pastel", "~> 0.8" # Terminal colors
40
+ spec.add_dependency "thor", "~> 1.3" # CLI framework
41
+ spec.add_dependency "tty-progressbar", "~> 0.18" # Progress bars
42
+ spec.add_dependency "tty-prompt", "~> 0.23" # Interactive prompts
43
+ spec.add_dependency "tty-table", "~> 0.12" # Table formatting
44
+
45
+ # Configuration and data management
46
+ spec.add_dependency "dry-configurable", "~> 1.0" # Configuration management
47
+ spec.add_dependency "sqlite3", "~> 1.6" # Session database
48
+ spec.add_dependency "zeitwerk", "~> 2.6" # Code loading
49
+
50
+ # Template engine (secure, sandboxed)
51
+ spec.add_dependency "liquid", "~> 5.4" # Safe template processing
52
+
53
+ # MCP server dependencies
54
+ spec.add_dependency "async", "~> 2.0" # Async operations
55
+ spec.add_dependency "json-schema", "~> 4.0" # Schema validation
56
+
57
+ # Security and encryption
58
+ spec.add_dependency "bcrypt", "~> 3.1" # Password hashing
59
+ spec.add_dependency "openssl", ">= 3.0" # Encryption support
60
+ spec.add_dependency "ostruct" # OpenStruct for Ruby 3.5+ compatibility
61
+
62
+ # File system operations
63
+ spec.add_dependency "listen", "~> 3.8" # File watching for config cache
64
+ spec.add_dependency "parallel", "~> 1.23" # Parallel execution
65
+
66
+ # Development dependencies
67
+ spec.add_development_dependency "aruba", "~> 2.1" # CLI testing
68
+ spec.add_development_dependency "benchmark" # Benchmark for Ruby 3.5+ compatibility
69
+ spec.add_development_dependency "benchmark-ips", "~> 2.12" # Performance benchmarking
70
+ spec.add_development_dependency "bundler", "~> 2.4"
71
+ spec.add_development_dependency "climate_control", "~> 1.2" # Environment variable testing
72
+ spec.add_development_dependency "faker", "~> 3.2" # Test data generation
73
+ spec.add_development_dependency "memory_profiler", "~> 1.0" # Memory profiling
74
+ spec.add_development_dependency "rake", "~> 13.0"
75
+ spec.add_development_dependency "rspec", "~> 3.12"
76
+ spec.add_development_dependency "rubocop", "~> 1.50" # Code linting
77
+ spec.add_development_dependency "rubocop-performance", "~> 1.16"
78
+ spec.add_development_dependency "rubocop-rspec", "~> 2.19"
79
+ spec.add_development_dependency "simplecov", "~> 0.22" # Code coverage
80
+ spec.add_development_dependency "vcr", "~> 6.2" # HTTP interaction recording
81
+ spec.add_development_dependency "webmock", "~> 3.19" # HTTP mocking for MCP tests
82
+
83
+ # For more information and examples about making a new gem, check out our
84
+ # guide at: https://bundler.io/guides/creating_gem.html
85
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sxn
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.2.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ernest Sim
@@ -518,6 +518,7 @@ files:
518
518
  - lib/sxn/core/config_manager.rb
519
519
  - lib/sxn/core/project_manager.rb
520
520
  - lib/sxn/core/rules_manager.rb
521
+ - lib/sxn/core/session_config.rb
521
522
  - lib/sxn/core/session_manager.rb
522
523
  - lib/sxn/core/worktree_manager.rb
523
524
  - lib/sxn/database.rb
@@ -558,6 +559,7 @@ files:
558
559
  - lib/sxn/version.rb
559
560
  - rbs_collection.lock.yaml
560
561
  - rbs_collection.yaml
562
+ - script/setup-hooks
561
563
  - scripts/test.sh
562
564
  - sig/external/liquid.rbs
563
565
  - sig/external/thor.rbs
@@ -609,6 +611,7 @@ files:
609
611
  - sig/sxn/ui/prompt.rbs
610
612
  - sig/sxn/ui/table.rbs
611
613
  - sig/sxn/version.rbs
614
+ - sxn.gemspec
612
615
  homepage: https://github.com/idl3/sxn
613
616
  licenses:
614
617
  - MIT