sxn 0.2.2 โ 0.2.3
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/CHANGELOG.md +20 -0
- data/Gemfile.lock +1 -1
- data/README.md +76 -8
- data/lib/sxn/commands/worktrees.rb +23 -4
- data/lib/sxn/core/config_manager.rb +13 -1
- data/lib/sxn/core/worktree_manager.rb +103 -4
- data/lib/sxn/version.rb +1 -1
- data/script/setup-hooks +52 -0
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6c2adba72c82bfd3b3b92c29bf497cc0c486b16261edc38c103135a7391525a4
|
4
|
+
data.tar.gz: fa079e0580d51496bf4d5f4af242b291847854a1d6dd6f17918730b463bb2c9d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d8a67b4261fb520b7b64c3c039dcabd5f47f1ae6ca377d1fa4d0a6d0dd9ac2574a83cc0024923273a754527c9e0cba96c75e0923918d1fd997ba62f8f1dd6ae7
|
7
|
+
data.tar.gz: 79218840d95ce17d7a62d40e2f4cfca09498973b784677175ec1cbf67a9ed801074fad18a716dba2a16f89c7499a030b53e3c9c5fbf1b5f412b7cbd0530cd628
|
data/CHANGELOG.md
CHANGED
@@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
7
|
|
8
|
+
## [0.2.3] - 2025-09-16
|
9
|
+
|
10
|
+
### Added
|
11
|
+
- Smart branch defaults: worktrees now use session name as default branch
|
12
|
+
- Remote branch tracking with `remote:` prefix syntax (e.g., `sxn worktree add project remote:origin/feature`)
|
13
|
+
- Automatic orphaned worktree recovery and cleanup
|
14
|
+
- Enhanced error messages with actionable suggestions
|
15
|
+
|
16
|
+
### Changed
|
17
|
+
- Improved worktree creation logic to handle existing/orphaned states
|
18
|
+
- Better error handling for remote branch operations
|
19
|
+
- Updated CLI documentation with new branch options
|
20
|
+
|
21
|
+
### Fixed
|
22
|
+
- Orphaned worktree cleanup now works for both existing and missing directories
|
23
|
+
- Worktree creation properly handles branch conflicts
|
24
|
+
- Test suite compatibility with new worktree features
|
25
|
+
|
8
26
|
## [0.2.1] - 2025-01-20
|
9
27
|
|
10
28
|
### Fixed
|
@@ -53,5 +71,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
53
71
|
- Initial placeholder release
|
54
72
|
- Basic gem structure
|
55
73
|
|
74
|
+
[0.2.3]: https://github.com/idl3/sxn/compare/v0.2.1...v0.2.3
|
75
|
+
[0.2.1]: https://github.com/idl3/sxn/compare/v0.2.0...v0.2.1
|
56
76
|
[0.2.0]: https://github.com/idl3/sxn/compare/v0.1.0...v0.2.0
|
57
77
|
[0.1.0]: https://github.com/idl3/sxn/releases/tag/v0.1.0
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -1,10 +1,10 @@
|
|
1
|
-
#
|
1
|
+
# sxn
|
2
2
|
|
3
3
|
[](https://github.com/idl3/sxn/actions/workflows/ci.yml)
|
4
4
|
[](https://www.ruby-lang.org)
|
5
5
|
[](LICENSE.txt)
|
6
6
|
|
7
|
-
|
7
|
+
sxn is a powerful session management tool for multi-repository development. It helps developers manage complex development environments with multiple git repositories, providing isolated workspaces, automatic project setup, and intelligent session management.
|
8
8
|
|
9
9
|
## Features
|
10
10
|
|
@@ -38,7 +38,7 @@ gem install sxn
|
|
38
38
|
|
39
39
|
## Quick Start
|
40
40
|
|
41
|
-
### Initialize
|
41
|
+
### Initialize sxn in your workspace
|
42
42
|
|
43
43
|
```bash
|
44
44
|
sxn init
|
@@ -135,7 +135,7 @@ sxn rules apply my-app
|
|
135
135
|
|
136
136
|
## Configuration
|
137
137
|
|
138
|
-
|
138
|
+
sxn stores its configuration in `.sxn/config.yml` in your workspace:
|
139
139
|
|
140
140
|
```yaml
|
141
141
|
sessions_folder: .sxn-sessions
|
@@ -168,7 +168,7 @@ rules:
|
|
168
168
|
|
169
169
|
## Templates
|
170
170
|
|
171
|
-
|
171
|
+
sxn includes templates for common project types:
|
172
172
|
|
173
173
|
- **Rails**: CLAUDE.md, database.yml, session-info.md
|
174
174
|
- **JavaScript**: README.md, session-info.md
|
@@ -178,19 +178,87 @@ Templates use Liquid syntax and have access to session, project, and environment
|
|
178
178
|
|
179
179
|
## Development
|
180
180
|
|
181
|
-
|
181
|
+
### Setup
|
182
182
|
|
183
|
-
|
183
|
+
After cloning the repository, run:
|
184
|
+
|
185
|
+
```bash
|
186
|
+
bundle install
|
187
|
+
./script/setup-hooks # Set up git hooks for automated checks
|
188
|
+
```
|
189
|
+
|
190
|
+
### Testing and Code Quality
|
191
|
+
|
192
|
+
**Important**: All code must pass tests and linting before being pushed to the repository.
|
193
|
+
|
194
|
+
#### Running Tests
|
184
195
|
|
185
196
|
```bash
|
186
197
|
# Run all tests
|
187
198
|
bundle exec rspec
|
188
199
|
|
200
|
+
# Run tests in parallel (faster)
|
201
|
+
bundle exec rake parallel:spec
|
202
|
+
|
203
|
+
# Run with coverage report
|
204
|
+
bundle exec rake parallel:spec_with_coverage
|
205
|
+
```
|
206
|
+
|
207
|
+
#### Code Style
|
208
|
+
|
209
|
+
```bash
|
210
|
+
# Check code style
|
211
|
+
bundle exec rubocop
|
212
|
+
|
213
|
+
# Auto-fix code style issues
|
214
|
+
bundle exec rubocop -a
|
215
|
+
```
|
216
|
+
|
217
|
+
#### Git Hooks
|
218
|
+
|
219
|
+
The project includes a pre-push hook that automatically runs RuboCop and RSpec before allowing pushes. To set it up:
|
220
|
+
|
221
|
+
```bash
|
222
|
+
./script/setup-hooks
|
223
|
+
```
|
224
|
+
|
225
|
+
To bypass hooks in emergency situations (not recommended):
|
226
|
+
```bash
|
227
|
+
git push --no-verify
|
228
|
+
```
|
229
|
+
|
230
|
+
### Development Workflow
|
231
|
+
|
232
|
+
1. Make your changes
|
233
|
+
2. Run `bundle exec rubocop -a` to fix any style issues
|
234
|
+
3. Run `bundle exec rspec` to ensure tests pass
|
235
|
+
4. Commit your changes
|
236
|
+
5. Push (pre-push hooks will run automatically)
|
237
|
+
|
238
|
+
### Contributing
|
239
|
+
|
240
|
+
1. Fork the repository
|
241
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
242
|
+
3. Make your changes
|
243
|
+
4. Ensure tests pass and code style is correct
|
244
|
+
5. Commit your changes with meaningful commit messages
|
245
|
+
6. Push to the branch (`git push origin feature/amazing-feature`)
|
246
|
+
7. Open a Pull Request
|
247
|
+
|
248
|
+
### Additional Testing Options
|
249
|
+
|
250
|
+
```bash
|
251
|
+
# Run all tests in parallel (recommended for speed)
|
252
|
+
bundle exec parallel_rspec spec/
|
253
|
+
|
254
|
+
# Run all tests sequentially
|
255
|
+
bundle exec rspec
|
256
|
+
|
189
257
|
# Run only unit tests
|
190
258
|
bundle exec rspec spec/unit
|
191
259
|
|
192
260
|
# Run with coverage
|
193
|
-
ENABLE_SIMPLECOV=true bundle exec
|
261
|
+
ENABLE_SIMPLECOV=true bundle exec parallel_rspec spec/
|
194
262
|
```
|
195
263
|
|
196
264
|
### Type Checking
|
@@ -19,7 +19,25 @@ module Sxn
|
|
19
19
|
@worktree_manager = Sxn::Core::WorktreeManager.new(@config_manager, @session_manager)
|
20
20
|
end
|
21
21
|
|
22
|
-
desc "add PROJECT [BRANCH]", "Add worktree to current session"
|
22
|
+
desc "add PROJECT [BRANCH]", "Add worktree to current session (defaults branch to session name)"
|
23
|
+
long_desc <<-DESC
|
24
|
+
Add a worktree for a project to the current session.
|
25
|
+
|
26
|
+
Branch options:
|
27
|
+
- No branch specified: Uses the session name as the branch name
|
28
|
+
- Branch name: Creates or checks out the specified branch
|
29
|
+
- remote:<branch>: Fetches and tracks the remote branch
|
30
|
+
|
31
|
+
Examples:
|
32
|
+
- sxn worktree add atlas-core
|
33
|
+
Creates worktree with branch name matching current session
|
34
|
+
|
35
|
+
- sxn worktree add atlas-core feature-branch
|
36
|
+
Creates worktree with specified branch name
|
37
|
+
|
38
|
+
- sxn worktree add atlas-core remote:origin/main
|
39
|
+
Fetches and tracks the remote branch
|
40
|
+
DESC
|
23
41
|
option :session, type: :string, aliases: "-s", desc: "Target session (defaults to current)"
|
24
42
|
option :apply_rules, type: :boolean, default: true, desc: "Apply project rules after creation"
|
25
43
|
option :interactive, type: :boolean, aliases: "-i", desc: "Interactive mode"
|
@@ -33,10 +51,11 @@ module Sxn
|
|
33
51
|
return if project_name.nil?
|
34
52
|
end
|
35
53
|
|
36
|
-
# Interactive branch selection if not provided
|
54
|
+
# Interactive branch selection if not provided and interactive mode
|
55
|
+
# Note: If branch is nil, WorktreeManager will use session name as default
|
37
56
|
if options[:interactive] && branch.nil?
|
38
|
-
|
39
|
-
branch = @prompt.branch_name("Enter branch name:", default:
|
57
|
+
session_name = options[:session] || @config_manager.current_session
|
58
|
+
branch = @prompt.branch_name("Enter branch name:", default: session_name)
|
40
59
|
end
|
41
60
|
|
42
61
|
session_name = options[:session] || @config_manager.current_session
|
@@ -3,6 +3,7 @@
|
|
3
3
|
require "fileutils"
|
4
4
|
require "yaml"
|
5
5
|
require "pathname"
|
6
|
+
require "ostruct"
|
6
7
|
|
7
8
|
module Sxn
|
8
9
|
module Core
|
@@ -40,7 +41,10 @@ module Sxn
|
|
40
41
|
raise Sxn::ConfigurationError, "Project not initialized. Run 'sxn init' first." unless initialized?
|
41
42
|
|
42
43
|
discovery = Sxn::Config::ConfigDiscovery.new(@base_path)
|
43
|
-
discovery.discover_config
|
44
|
+
config_hash = discovery.discover_config
|
45
|
+
|
46
|
+
# Convert nested hashes to OpenStruct recursively
|
47
|
+
config_to_struct(config_hash)
|
44
48
|
end
|
45
49
|
|
46
50
|
def update_current_session(session_name)
|
@@ -194,6 +198,14 @@ module Sxn
|
|
194
198
|
|
195
199
|
private
|
196
200
|
|
201
|
+
def config_to_struct(obj)
|
202
|
+
return obj unless obj.is_a?(Hash)
|
203
|
+
|
204
|
+
OpenStruct.new(
|
205
|
+
obj.transform_values { |v| config_to_struct(v) }
|
206
|
+
)
|
207
|
+
end
|
208
|
+
|
197
209
|
def sessions_folder_relative_path
|
198
210
|
return ".sxn" unless @sessions_folder
|
199
211
|
|
@@ -25,8 +25,30 @@ 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
|
-
#
|
29
|
-
branch
|
28
|
+
# Determine branch name
|
29
|
+
# If no branch specified, use session name as the branch name
|
30
|
+
# If branch starts with "remote:", handle remote branch tracking
|
31
|
+
if branch.nil?
|
32
|
+
branch = session_name
|
33
|
+
elsif branch.start_with?("remote:")
|
34
|
+
remote_branch = branch.sub("remote:", "")
|
35
|
+
# Fetch the remote branch first
|
36
|
+
begin
|
37
|
+
fetch_remote_branch(project[:path], remote_branch)
|
38
|
+
branch = remote_branch
|
39
|
+
rescue StandardError => e
|
40
|
+
raise Sxn::WorktreeCreationError, "Failed to fetch remote branch: #{e.message}"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
if ENV["SXN_DEBUG"]
|
45
|
+
puts "[DEBUG] Adding worktree:"
|
46
|
+
puts " Project: #{project_name}"
|
47
|
+
puts " Project path: #{project[:path]}"
|
48
|
+
puts " Session: #{session_name}"
|
49
|
+
puts " Session path: #{session[:path]}"
|
50
|
+
puts " Branch: #{branch}"
|
51
|
+
end
|
30
52
|
|
31
53
|
# Check if worktree already exists in this session
|
32
54
|
existing_worktrees = @session_manager.get_session_worktrees(session_name)
|
@@ -38,7 +60,12 @@ module Sxn
|
|
38
60
|
# Create worktree path
|
39
61
|
worktree_path = File.join(session[:path], project_name)
|
40
62
|
|
63
|
+
puts " Worktree path: #{worktree_path}" if ENV["SXN_DEBUG"]
|
64
|
+
|
41
65
|
begin
|
66
|
+
# Handle orphaned worktree if it exists
|
67
|
+
handle_orphaned_worktree(project[:path], worktree_path)
|
68
|
+
|
42
69
|
# Create the worktree
|
43
70
|
create_git_worktree(project[:path], worktree_path, branch)
|
44
71
|
|
@@ -169,6 +196,47 @@ module Sxn
|
|
169
196
|
|
170
197
|
private
|
171
198
|
|
199
|
+
def handle_orphaned_worktree(project_path, worktree_path)
|
200
|
+
Dir.chdir(project_path) do
|
201
|
+
# Try to prune worktrees first
|
202
|
+
system("git worktree prune", out: File::NULL, err: File::NULL)
|
203
|
+
|
204
|
+
# Check if this worktree is registered (whether it exists or not)
|
205
|
+
output = `git worktree list --porcelain 2>/dev/null`
|
206
|
+
if output.include?(worktree_path)
|
207
|
+
# Force remove the orphaned/existing worktree
|
208
|
+
system("git worktree remove --force #{Shellwords.escape(worktree_path)}", out: File::NULL, err: File::NULL)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
# Remove the directory if it still exists
|
213
|
+
FileUtils.rm_rf(worktree_path)
|
214
|
+
end
|
215
|
+
|
216
|
+
def fetch_remote_branch(project_path, branch_name)
|
217
|
+
Dir.chdir(project_path) do
|
218
|
+
# First, fetch all remotes to ensure we have the latest branches
|
219
|
+
raise "Failed to fetch remote branches" unless system("git fetch --all", out: File::NULL, err: File::NULL)
|
220
|
+
|
221
|
+
# Check if the branch exists on any remote
|
222
|
+
remotes = `git remote`.lines.map(&:strip)
|
223
|
+
branch_found = false
|
224
|
+
|
225
|
+
remotes.each do |remote|
|
226
|
+
next unless system("git show-ref --verify --quiet refs/remotes/#{remote}/#{Shellwords.escape(branch_name)}",
|
227
|
+
out: File::NULL, err: File::NULL)
|
228
|
+
|
229
|
+
branch_found = true
|
230
|
+
# Set up tracking for the remote branch
|
231
|
+
system("git branch --track #{Shellwords.escape(branch_name)} #{remote}/#{Shellwords.escape(branch_name)}",
|
232
|
+
out: File::NULL, err: File::NULL)
|
233
|
+
break
|
234
|
+
end
|
235
|
+
|
236
|
+
raise "Remote branch '#{branch_name}' not found on any remote" unless branch_found
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
172
240
|
def create_git_worktree(project_path, worktree_path, branch)
|
173
241
|
Dir.chdir(project_path) do
|
174
242
|
# Check if branch exists
|
@@ -183,8 +251,39 @@ module Sxn
|
|
183
251
|
["git", "worktree", "add", "-b", branch, worktree_path]
|
184
252
|
end
|
185
253
|
|
186
|
-
|
187
|
-
|
254
|
+
# Capture stderr for better error messages
|
255
|
+
require "open3"
|
256
|
+
stdout, stderr, status = Open3.capture3(*cmd)
|
257
|
+
|
258
|
+
unless status.success?
|
259
|
+
error_msg = stderr.empty? ? stdout : stderr
|
260
|
+
error_msg = "Git worktree command failed" if error_msg.strip.empty?
|
261
|
+
|
262
|
+
# Add more context to common errors
|
263
|
+
if error_msg.include?("already exists")
|
264
|
+
error_msg += "\nTry removing the existing worktree first with: sxn worktree remove #{File.basename(worktree_path)}"
|
265
|
+
elsif error_msg.include?("is already checked out")
|
266
|
+
error_msg += "\nThis branch is already checked out in another worktree"
|
267
|
+
elsif error_msg.include?("not a git repository")
|
268
|
+
error_msg = "Project '#{File.basename(project_path)}' is not a git repository"
|
269
|
+
elsif error_msg.include?("fatal: invalid reference")
|
270
|
+
# This typically means the branch doesn't exist and we're trying to create from a non-existent base
|
271
|
+
error_msg += "\nMake sure the repository has at least one commit or specify an existing branch"
|
272
|
+
elsif error_msg.include?("fatal:")
|
273
|
+
# Extract just the fatal error message for cleaner output
|
274
|
+
error_msg = error_msg.lines.grep(/fatal:/).first&.strip || error_msg
|
275
|
+
end
|
276
|
+
|
277
|
+
if ENV["SXN_DEBUG"]
|
278
|
+
puts "[DEBUG] Git worktree command failed:"
|
279
|
+
puts " Command: #{cmd.join(" ")}"
|
280
|
+
puts " Directory: #{project_path}"
|
281
|
+
puts " STDOUT: #{stdout}"
|
282
|
+
puts " STDERR: #{stderr}"
|
283
|
+
end
|
284
|
+
|
285
|
+
raise error_msg
|
286
|
+
end
|
188
287
|
end
|
189
288
|
end
|
190
289
|
|
data/lib/sxn/version.rb
CHANGED
data/script/setup-hooks
ADDED
@@ -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"
|
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.
|
4
|
+
version: 0.2.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ernest Sim
|
@@ -558,6 +558,7 @@ files:
|
|
558
558
|
- lib/sxn/version.rb
|
559
559
|
- rbs_collection.lock.yaml
|
560
560
|
- rbs_collection.yaml
|
561
|
+
- script/setup-hooks
|
561
562
|
- scripts/test.sh
|
562
563
|
- sig/external/liquid.rbs
|
563
564
|
- sig/external/thor.rbs
|