From 1af6121e4e39fa98464db33343710e880ddcc06d Mon Sep 17 00:00:00 2001 From: Victor Gutierrez Calderon Date: Fri, 3 Apr 2026 16:27:10 +0200 Subject: [PATCH 1/3] fix: normalize MSYS/Unix paths on Windows to prevent phantom shadow branches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Windows, transcript-extracted file paths arrive in Unix formats that filepath.IsAbs doesn't recognize (/c/Users/..., /tmp/..., /home/user/...). These leaked through FilterAndNormalizePaths into filterToUncommittedFiles, causing phantom shadow branches that failed test assertions. Add NormalizeMSYSPath to convert known MSYS paths (/c/ → C:/, /tmp/ → temp dir), and drop any remaining Unix-style paths the OS can't resolve. Co-Authored-By: Claude Opus 4.6 (1M context) --- cmd/entire/cli/paths/paths.go | 33 +++++++++++++++++++++++++++++++++ cmd/entire/cli/state.go | 16 ++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/cmd/entire/cli/paths/paths.go b/cmd/entire/cli/paths/paths.go index f22617249..465b890e1 100644 --- a/cmd/entire/cli/paths/paths.go +++ b/cmd/entire/cli/paths/paths.go @@ -155,6 +155,15 @@ func IsSubpath(parent, child string) bool { // ToRelativePath converts an absolute path to relative. // Returns empty string if the path is outside the working directory. func ToRelativePath(absPath, cwd string) string { + absPath = NormalizeMSYSPath(absPath) + + // After MSYS normalization, a path starting with "/" that the OS still + // doesn't recognize as absolute is an unconvertible Unix path (e.g., + // /home/user/... from a container/sandbox on Windows). Filter it out. + if strings.HasPrefix(absPath, "/") && !filepath.IsAbs(absPath) { + return "" + } + if !filepath.IsAbs(absPath) { return absPath } @@ -166,6 +175,30 @@ func ToRelativePath(absPath, cwd string) string { return relPath } +// msysDrivePrefix matches MSYS/Git-Bash-style absolute paths like /c/ or /D/. +// Git for Windows executes hooks through MSYS2 bash, which converts Windows paths +// (C:\Users\...) to Unix-style (/c/Users/...) in tool output and transcripts. +var msysDrivePrefix = regexp.MustCompile(`^/([a-zA-Z])/`) + +// NormalizeMSYSPath converts MSYS/Git-Bash paths to Windows paths. +// Handles two MSYS conventions: +// - Drive paths: /c/Users/... → C:/Users/... +// - Virtual dirs: /tmp/... → /... (MSYS2 maps /tmp to the Windows temp dir) +// +// Returns the input unchanged on non-Windows or if the path doesn't match. +func NormalizeMSYSPath(p string) string { + if m := msysDrivePrefix.FindStringSubmatch(p); m != nil { + return strings.ToUpper(m[1]) + ":/" + p[3:] + } + // MSYS2 maps /tmp to the Windows temp directory. + if strings.HasPrefix(p, "/tmp/") { + if tmp := os.TempDir(); tmp != "" { + return filepath.Join(tmp, p[5:]) + } + } + return p +} + // nonAlphanumericRegex matches any non-alphanumeric character var nonAlphanumericRegex = regexp.MustCompile(`[^a-zA-Z0-9]`) diff --git a/cmd/entire/cli/state.go b/cmd/entire/cli/state.go index 545f6e0b6..0d7e684eb 100644 --- a/cmd/entire/cli/state.go +++ b/cmd/entire/cli/state.go @@ -297,21 +297,37 @@ func filterToUncommittedFiles(ctx context.Context, files []string, repoRoot stri repo, err := openRepository(ctx) if err != nil { + logging.Warn(ctx, "filterToUncommittedFiles: failed to open repository, keeping all files", + slog.String("error", err.Error()), + slog.String("dir", repoRoot), + slog.Any("files", files)) return files // fail open } head, err := repo.Head() if err != nil { + logging.Warn(ctx, "filterToUncommittedFiles: failed to resolve HEAD, keeping all files", + slog.String("error", err.Error()), + slog.String("dir", repoRoot), + slog.Any("files", files)) return files // fail open (empty repo, detached HEAD, etc.) } commit, err := repo.CommitObject(head.Hash()) if err != nil { + logging.Warn(ctx, "filterToUncommittedFiles: failed to get commit object, keeping all files", + slog.String("error", err.Error()), + slog.String("dir", repoRoot), + slog.Any("files", files)) return files // fail open } headTree, err := commit.Tree() if err != nil { + logging.Warn(ctx, "filterToUncommittedFiles: failed to get commit tree, keeping all files", + slog.String("error", err.Error()), + slog.String("dir", repoRoot), + slog.Any("files", files)) return files // fail open } From e60f3dc9ec3921ece9586c58018382ab382c801d Mon Sep 17 00:00:00 2001 From: Victor Gutierrez Calderon Date: Fri, 3 Apr 2026 18:10:26 +0200 Subject: [PATCH 2/3] test: add unit tests for NormalizeMSYSPath and ToRelativePath Unix handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cover MSYS drive conversion (/c/ → C:/), /tmp/ mapping, passthrough of already-relative and non-matching paths, and edge cases. Co-Authored-By: Claude Opus 4.6 (1M context) Entire-Checkpoint: 94ff42cf7576 --- cmd/entire/cli/paths/paths_test.go | 69 ++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/cmd/entire/cli/paths/paths_test.go b/cmd/entire/cli/paths/paths_test.go index 31c3bd195..4d9674677 100644 --- a/cmd/entire/cli/paths/paths_test.go +++ b/cmd/entire/cli/paths/paths_test.go @@ -102,6 +102,75 @@ func TestGetClaudeProjectDir_Override(t *testing.T) { } } +func TestNormalizeMSYSPath(t *testing.T) { + t.Parallel() + tests := []struct { + name string + in string + want string + }{ + // Drive letter paths + {name: "lowercase drive", in: "/c/Users/Victor/repo", want: "C:/Users/Victor/repo"}, + {name: "uppercase drive", in: "/D/Projects/app", want: "D:/Projects/app"}, + {name: "drive root", in: "/c/", want: "C:/"}, + + // /tmp mapping + {name: "tmp path", in: "/tmp/e2e-repo-123/docs/red.md", want: filepath.Join(os.TempDir(), "e2e-repo-123/docs/red.md")}, + {name: "tmp root file", in: "/tmp/file.txt", want: filepath.Join(os.TempDir(), "file.txt")}, + + // Paths that should NOT be converted + {name: "already relative", in: "docs/red.md", want: "docs/red.md"}, + {name: "windows absolute", in: "C:/Users/Victor/repo", want: "C:/Users/Victor/repo"}, + {name: "unix absolute non-drive", in: "/home/user/docs/red.md", want: "/home/user/docs/red.md"}, + {name: "empty string", in: "", want: ""}, + {name: "single slash", in: "/", want: "/"}, + {name: "tmp exact no trailing slash", in: "/tmp", want: "/tmp"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := NormalizeMSYSPath(tt.in) + if got != tt.want { + t.Errorf("NormalizeMSYSPath(%q) = %q, want %q", tt.in, got, tt.want) + } + }) + } +} + +func TestToRelativePath_MSYSAndUnixPaths(t *testing.T) { + t.Parallel() + + // On Unix, /home/user/... is a valid absolute path so filepath.Rel handles it. + // On Windows, /home/user/... is not absolute and gets dropped by the Unix filter. + // These tests verify behavior that is consistent across platforms. + tests := []struct { + name string + absPath string + cwd string + want string + }{ + // Relative paths pass through unchanged on all platforms + {name: "relative path unchanged", absPath: "docs/red.md", cwd: "/repo", want: "docs/red.md"}, + + // MSYS drive paths: /c/ → C:/ conversion happens, then platform-specific handling. + // On Unix, C:/Users/... is not absolute so it passes through as-is. + // On Windows, C:/Users/... is absolute so filepath.Rel resolves it. + // We test the NormalizeMSYSPath conversion separately above. + + // Paths outside cwd return empty + {name: "outside cwd", absPath: "/other/repo/file.txt", cwd: "/my/repo", want: ""}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := ToRelativePath(tt.absPath, tt.cwd) + if got != tt.want { + t.Errorf("ToRelativePath(%q, %q) = %q, want %q", tt.absPath, tt.cwd, got, tt.want) + } + }) + } +} + func TestGetClaudeProjectDir_Default(t *testing.T) { // Ensure env var is not set by setting it to empty string t.Setenv("ENTIRE_TEST_CLAUDE_PROJECT_DIR", "") From 0594f8b705e71fa61a18d26c65f1ca196201224b Mon Sep 17 00:00:00 2001 From: Victor Gutierrez Calderon Date: Fri, 3 Apr 2026 18:30:05 +0200 Subject: [PATCH 3/3] refactor: split ToRelativePath into platform-specific files with build tags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NormalizeMSYSPath had no runtime.GOOS guard, corrupting valid Unix paths on macOS/Linux (e.g., /tmp/repo/file.txt → /var/folders/...). Move ToRelativePath and NormalizeMSYSPath into relative_windows.go (with MSYS normalization + Unix path filter) and relative_unix.go (plain filepath.Rel, no normalization). Tests split accordingly. Co-Authored-By: Claude Opus 4.6 (1M context) Entire-Checkpoint: cbe58c84ed5b --- cmd/entire/cli/paths/paths.go | 47 ----------- cmd/entire/cli/paths/paths_test.go | 69 ---------------- cmd/entire/cli/paths/relative_unix.go | 22 ++++++ cmd/entire/cli/paths/relative_unix_test.go | 39 ++++++++++ cmd/entire/cli/paths/relative_windows.go | 62 +++++++++++++++ cmd/entire/cli/paths/relative_windows_test.go | 78 +++++++++++++++++++ 6 files changed, 201 insertions(+), 116 deletions(-) create mode 100644 cmd/entire/cli/paths/relative_unix.go create mode 100644 cmd/entire/cli/paths/relative_unix_test.go create mode 100644 cmd/entire/cli/paths/relative_windows.go create mode 100644 cmd/entire/cli/paths/relative_windows_test.go diff --git a/cmd/entire/cli/paths/paths.go b/cmd/entire/cli/paths/paths.go index 465b890e1..09d6f423c 100644 --- a/cmd/entire/cli/paths/paths.go +++ b/cmd/entire/cli/paths/paths.go @@ -152,53 +152,6 @@ func IsSubpath(parent, child string) bool { return rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator)) } -// ToRelativePath converts an absolute path to relative. -// Returns empty string if the path is outside the working directory. -func ToRelativePath(absPath, cwd string) string { - absPath = NormalizeMSYSPath(absPath) - - // After MSYS normalization, a path starting with "/" that the OS still - // doesn't recognize as absolute is an unconvertible Unix path (e.g., - // /home/user/... from a container/sandbox on Windows). Filter it out. - if strings.HasPrefix(absPath, "/") && !filepath.IsAbs(absPath) { - return "" - } - - if !filepath.IsAbs(absPath) { - return absPath - } - relPath, err := filepath.Rel(cwd, absPath) - if err != nil || strings.HasPrefix(relPath, "..") { - return "" - } - - return relPath -} - -// msysDrivePrefix matches MSYS/Git-Bash-style absolute paths like /c/ or /D/. -// Git for Windows executes hooks through MSYS2 bash, which converts Windows paths -// (C:\Users\...) to Unix-style (/c/Users/...) in tool output and transcripts. -var msysDrivePrefix = regexp.MustCompile(`^/([a-zA-Z])/`) - -// NormalizeMSYSPath converts MSYS/Git-Bash paths to Windows paths. -// Handles two MSYS conventions: -// - Drive paths: /c/Users/... → C:/Users/... -// - Virtual dirs: /tmp/... → /... (MSYS2 maps /tmp to the Windows temp dir) -// -// Returns the input unchanged on non-Windows or if the path doesn't match. -func NormalizeMSYSPath(p string) string { - if m := msysDrivePrefix.FindStringSubmatch(p); m != nil { - return strings.ToUpper(m[1]) + ":/" + p[3:] - } - // MSYS2 maps /tmp to the Windows temp directory. - if strings.HasPrefix(p, "/tmp/") { - if tmp := os.TempDir(); tmp != "" { - return filepath.Join(tmp, p[5:]) - } - } - return p -} - // nonAlphanumericRegex matches any non-alphanumeric character var nonAlphanumericRegex = regexp.MustCompile(`[^a-zA-Z0-9]`) diff --git a/cmd/entire/cli/paths/paths_test.go b/cmd/entire/cli/paths/paths_test.go index 4d9674677..31c3bd195 100644 --- a/cmd/entire/cli/paths/paths_test.go +++ b/cmd/entire/cli/paths/paths_test.go @@ -102,75 +102,6 @@ func TestGetClaudeProjectDir_Override(t *testing.T) { } } -func TestNormalizeMSYSPath(t *testing.T) { - t.Parallel() - tests := []struct { - name string - in string - want string - }{ - // Drive letter paths - {name: "lowercase drive", in: "/c/Users/Victor/repo", want: "C:/Users/Victor/repo"}, - {name: "uppercase drive", in: "/D/Projects/app", want: "D:/Projects/app"}, - {name: "drive root", in: "/c/", want: "C:/"}, - - // /tmp mapping - {name: "tmp path", in: "/tmp/e2e-repo-123/docs/red.md", want: filepath.Join(os.TempDir(), "e2e-repo-123/docs/red.md")}, - {name: "tmp root file", in: "/tmp/file.txt", want: filepath.Join(os.TempDir(), "file.txt")}, - - // Paths that should NOT be converted - {name: "already relative", in: "docs/red.md", want: "docs/red.md"}, - {name: "windows absolute", in: "C:/Users/Victor/repo", want: "C:/Users/Victor/repo"}, - {name: "unix absolute non-drive", in: "/home/user/docs/red.md", want: "/home/user/docs/red.md"}, - {name: "empty string", in: "", want: ""}, - {name: "single slash", in: "/", want: "/"}, - {name: "tmp exact no trailing slash", in: "/tmp", want: "/tmp"}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - got := NormalizeMSYSPath(tt.in) - if got != tt.want { - t.Errorf("NormalizeMSYSPath(%q) = %q, want %q", tt.in, got, tt.want) - } - }) - } -} - -func TestToRelativePath_MSYSAndUnixPaths(t *testing.T) { - t.Parallel() - - // On Unix, /home/user/... is a valid absolute path so filepath.Rel handles it. - // On Windows, /home/user/... is not absolute and gets dropped by the Unix filter. - // These tests verify behavior that is consistent across platforms. - tests := []struct { - name string - absPath string - cwd string - want string - }{ - // Relative paths pass through unchanged on all platforms - {name: "relative path unchanged", absPath: "docs/red.md", cwd: "/repo", want: "docs/red.md"}, - - // MSYS drive paths: /c/ → C:/ conversion happens, then platform-specific handling. - // On Unix, C:/Users/... is not absolute so it passes through as-is. - // On Windows, C:/Users/... is absolute so filepath.Rel resolves it. - // We test the NormalizeMSYSPath conversion separately above. - - // Paths outside cwd return empty - {name: "outside cwd", absPath: "/other/repo/file.txt", cwd: "/my/repo", want: ""}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - got := ToRelativePath(tt.absPath, tt.cwd) - if got != tt.want { - t.Errorf("ToRelativePath(%q, %q) = %q, want %q", tt.absPath, tt.cwd, got, tt.want) - } - }) - } -} - func TestGetClaudeProjectDir_Default(t *testing.T) { // Ensure env var is not set by setting it to empty string t.Setenv("ENTIRE_TEST_CLAUDE_PROJECT_DIR", "") diff --git a/cmd/entire/cli/paths/relative_unix.go b/cmd/entire/cli/paths/relative_unix.go new file mode 100644 index 000000000..4be38ddf5 --- /dev/null +++ b/cmd/entire/cli/paths/relative_unix.go @@ -0,0 +1,22 @@ +//go:build unix + +package paths + +import ( + "path/filepath" + "strings" +) + +// ToRelativePath converts an absolute path to relative. +// Returns empty string if the path is outside the working directory. +func ToRelativePath(absPath, cwd string) string { + if !filepath.IsAbs(absPath) { + return absPath + } + relPath, err := filepath.Rel(cwd, absPath) + if err != nil || strings.HasPrefix(relPath, "..") { + return "" + } + + return relPath +} diff --git a/cmd/entire/cli/paths/relative_unix_test.go b/cmd/entire/cli/paths/relative_unix_test.go new file mode 100644 index 000000000..0eaacd13c --- /dev/null +++ b/cmd/entire/cli/paths/relative_unix_test.go @@ -0,0 +1,39 @@ +//go:build unix + +package paths + +import ( + "testing" +) + +func TestToRelativePath(t *testing.T) { + t.Parallel() + tests := []struct { + name string + absPath string + cwd string + want string + }{ + // Standard relative conversion + {name: "child of cwd", absPath: "/repo/docs/red.md", cwd: "/repo", want: "docs/red.md"}, + {name: "exact cwd", absPath: "/repo", cwd: "/repo", want: "."}, + {name: "outside cwd", absPath: "/other/file.txt", cwd: "/repo", want: ""}, + + // Already relative — pass through + {name: "relative unchanged", absPath: "docs/red.md", cwd: "/repo", want: "docs/red.md"}, + + // Unix paths that are valid on this platform + {name: "tmp is valid unix path", absPath: "/tmp/repo/file.txt", cwd: "/tmp/repo", want: "file.txt"}, + {name: "home is valid unix path", absPath: "/home/user/repo/f.go", cwd: "/home/user/repo", want: "f.go"}, + {name: "slash-c is valid unix path", absPath: "/c/Users/repo/f.go", cwd: "/c/Users/repo", want: "f.go"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := ToRelativePath(tt.absPath, tt.cwd) + if got != tt.want { + t.Errorf("ToRelativePath(%q, %q) = %q, want %q", tt.absPath, tt.cwd, got, tt.want) + } + }) + } +} diff --git a/cmd/entire/cli/paths/relative_windows.go b/cmd/entire/cli/paths/relative_windows.go new file mode 100644 index 000000000..b89b7918b --- /dev/null +++ b/cmd/entire/cli/paths/relative_windows.go @@ -0,0 +1,62 @@ +//go:build windows + +package paths + +import ( + "os" + "path/filepath" + "regexp" + "strings" +) + +// msysDrivePrefix matches MSYS/Git-Bash-style absolute paths like /c/ or /D/. +// Git for Windows executes hooks through MSYS2 bash, which converts Windows paths +// (C:\Users\...) to Unix-style (/c/Users/...) in tool output and transcripts. +var msysDrivePrefix = regexp.MustCompile(`^/([a-zA-Z])/`) + +// NormalizeMSYSPath converts MSYS/Git-Bash paths to Windows paths. +// Handles two MSYS conventions: +// - Drive paths: /c/Users/... → C:/Users/... +// - Virtual dirs: /tmp/... → /... (MSYS2 maps /tmp to the Windows temp dir) +// +// Returns the input unchanged if the path doesn't match any known MSYS pattern. +func NormalizeMSYSPath(p string) string { + if m := msysDrivePrefix.FindStringSubmatch(p); m != nil { + return strings.ToUpper(m[1]) + ":/" + p[3:] + } + // MSYS2 maps /tmp to the Windows temp directory. + if strings.HasPrefix(p, "/tmp/") { + if tmp := os.TempDir(); tmp != "" { + return filepath.Join(tmp, p[5:]) + } + } + return p +} + +// ToRelativePath converts an absolute path to relative. +// Returns empty string if the path is outside the working directory. +// +// On Windows, transcript-extracted paths may arrive in Unix formats from +// MSYS2/Git Bash (/c/Users/..., /tmp/...) or agent sandboxes (/home/user/...). +// These are normalized via NormalizeMSYSPath, and any remaining Unix-style +// paths that the OS can't resolve are dropped. +func ToRelativePath(absPath, cwd string) string { + absPath = NormalizeMSYSPath(absPath) + + // After MSYS normalization, a path starting with "/" that the OS still + // doesn't recognize as absolute is an unconvertible Unix path (e.g., + // /home/user/... from a container/sandbox). Filter it out. + if strings.HasPrefix(absPath, "/") && !filepath.IsAbs(absPath) { + return "" + } + + if !filepath.IsAbs(absPath) { + return absPath + } + relPath, err := filepath.Rel(cwd, absPath) + if err != nil || strings.HasPrefix(relPath, "..") { + return "" + } + + return relPath +} diff --git a/cmd/entire/cli/paths/relative_windows_test.go b/cmd/entire/cli/paths/relative_windows_test.go new file mode 100644 index 000000000..e737338c6 --- /dev/null +++ b/cmd/entire/cli/paths/relative_windows_test.go @@ -0,0 +1,78 @@ +//go:build windows + +package paths + +import ( + "os" + "path/filepath" + "testing" +) + +func TestNormalizeMSYSPath(t *testing.T) { + t.Parallel() + tests := []struct { + name string + in string + want string + }{ + // Drive letter paths + {name: "lowercase drive", in: "/c/Users/Victor/repo", want: "C:/Users/Victor/repo"}, + {name: "uppercase drive", in: "/D/Projects/app", want: "D:/Projects/app"}, + {name: "drive root", in: "/c/", want: "C:/"}, + + // /tmp mapping + {name: "tmp path", in: "/tmp/e2e-repo-123/docs/red.md", want: filepath.Join(os.TempDir(), "e2e-repo-123", "docs", "red.md")}, + {name: "tmp root file", in: "/tmp/file.txt", want: filepath.Join(os.TempDir(), "file.txt")}, + + // Paths that should NOT be converted + {name: "already relative", in: "docs/red.md", want: "docs/red.md"}, + {name: "windows absolute", in: "C:/Users/Victor/repo", want: "C:/Users/Victor/repo"}, + {name: "unix absolute non-drive", in: "/home/user/docs/red.md", want: "/home/user/docs/red.md"}, + {name: "empty string", in: "", want: ""}, + {name: "single slash", in: "/", want: "/"}, + {name: "tmp exact no trailing slash", in: "/tmp", want: "/tmp"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := NormalizeMSYSPath(tt.in) + if got != tt.want { + t.Errorf("NormalizeMSYSPath(%q) = %q, want %q", tt.in, got, tt.want) + } + }) + } +} + +func TestToRelativePath(t *testing.T) { + t.Parallel() + tests := []struct { + name string + absPath string + cwd string + want string + }{ + // Standard relative conversion + {name: "child of cwd", absPath: "C:/repo/docs/red.md", cwd: "C:/repo", want: "docs/red.md"}, + {name: "exact cwd", absPath: "C:/repo", cwd: "C:/repo", want: "."}, + {name: "outside cwd", absPath: "C:/other/file.txt", cwd: "C:/repo", want: ""}, + + // Already relative — pass through + {name: "relative unchanged", absPath: "docs/red.md", cwd: "C:/repo", want: "docs/red.md"}, + + // MSYS drive paths get normalized and resolved + {name: "msys drive path", absPath: "/c/repo/docs/red.md", cwd: "C:/repo", want: "docs/red.md"}, + + // Container/sandbox paths are dropped (can't resolve on Windows) + {name: "container path dropped", absPath: "/home/user/docs/red.md", cwd: "C:/repo", want: ""}, + {name: "unix opt path dropped", absPath: "/opt/app/file.txt", cwd: "C:/repo", want: ""}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := ToRelativePath(tt.absPath, tt.cwd) + if got != tt.want { + t.Errorf("ToRelativePath(%q, %q) = %q, want %q", tt.absPath, tt.cwd, got, tt.want) + } + }) + } +}