diff --git a/bin/wt-metadata-export b/bin/wt-metadata-export index ee6e5fd..42545dd 100755 --- a/bin/wt-metadata-export +++ b/bin/wt-metadata-export @@ -154,7 +154,7 @@ if [[ "$SKIP_CONFIRM" != "true" ]]; then echo fi -# Find all metadata directories and deduplicate +# Find all metadata directories and prune nested paths # Outputs: one path per line, with nested paths removed # (e.g., if .ijwb contains .idea, only .ijwb is listed) find_all_metadata_dirs() { @@ -166,34 +166,11 @@ find_all_metadata_dirs() { done < <(find "$SOURCE_DIR" -maxdepth 5 -type d -name "$pattern" 2>/dev/null) done - # No metadata found if [[ ${#all_paths[@]} -eq 0 ]]; then return fi - # Sort paths (shorter paths come first) - local sorted_paths - sorted_paths=$(printf '%s\n' "${all_paths[@]}" | sort) - - # Deduplicate: skip paths that are inside another metadata path - local kept_paths=() - while IFS= read -r _entry_path; do - [[ -z "$_entry_path" ]] && continue - local dominated=false - - for kept in ${kept_paths[@]+"${kept_paths[@]}"}; do - # Check if $_p is inside $kept (kept is a prefix of _p) - if [[ "$_entry_path" == "$kept/"* ]]; then - dominated=true - break - fi - done - - if [[ "$dominated" == "false" ]]; then - kept_paths+=("$_entry_path") - echo "$_entry_path" - fi - done <<< "$sorted_paths" + printf '%s\n' "${all_paths[@]}" | _wt_prune_nested_paths } echo "Finding and deduplicating metadata directories..." diff --git a/bin/wt-metadata-import b/bin/wt-metadata-import index 76f8e4e..85adffb 100755 --- a/bin/wt-metadata-import +++ b/bin/wt-metadata-import @@ -186,63 +186,69 @@ copy_real_metadata() { cp -a "$real_path" "$dst" } -# Import a single pattern -# Args: $1 = pattern name (e.g., ".ijwb") -import_pattern() { - local pattern="$1" - local count=0 - - echo "Importing '$pattern' directories..." - - # Find all matching directories (symlinks or real directories) under the source directory - # Use -L to follow symlinks (source path might be a symlink) - # Note: Use process substitution (< <(...)) instead of pipe to avoid subshell variable scope issues - while IFS= read -r -d '' META_SRC; do - # Parent dir relative to SOURCE_DIR - PARENT_SRC="$(dirname "$META_SRC")" - - # Handle pattern at root (PARENT_SRC == SOURCE_DIR) - if [[ "$PARENT_SRC" == "$SOURCE_DIR" ]]; then - REL_PATH="" - else - REL_PATH="${PARENT_SRC#"$SOURCE_DIR"/}" - fi +# Find all metadata directories across all patterns and prune nested paths. +# Uses -L to follow symlinks in the vault. +# Outputs: one path per line, with nested paths removed. +find_all_import_dirs() { + local all_paths=() + + for pattern in $WT_METADATA_PATTERNS; do + while IFS= read -r _entry_path; do + [[ -n "$_entry_path" ]] && all_paths+=("$_entry_path") + done < <(find -L "$SOURCE_DIR" \( -type l -o -type d \) -name "$pattern" -print 2>/dev/null) + done + + if [[ ${#all_paths[@]} -eq 0 ]]; then + return + fi - # Corresponding target parent - if [[ -z "$REL_PATH" ]]; then - TARGET_PARENT="$TARGET_DIR" - else - TARGET_PARENT="$TARGET_DIR/$REL_PATH" - fi - TARGET_META="$TARGET_PARENT/$pattern" + printf '%s\n' "${all_paths[@]}" | _wt_prune_nested_paths +} - echo " Found source: $META_SRC" - echo " -> Installing to: $TARGET_META" +# Import all metadata directories (pruned across patterns) +count=0 +while IFS= read -r META_SRC; do + [[ -z "$META_SRC" ]] && continue - mkdir -p "$TARGET_PARENT" + pattern="$(basename "$META_SRC")" + PARENT_SRC="$(dirname "$META_SRC")" - if [[ -e "$TARGET_META" || -L "$TARGET_META" ]]; then - rm -rf "$TARGET_META" - fi + if [[ "$PARENT_SRC" == "$SOURCE_DIR" ]]; then + REL_PATH="" + else + REL_PATH="${PARENT_SRC#"$SOURCE_DIR"/}" + fi - if ! copy_real_metadata "$META_SRC" "$TARGET_META"; then - echo " Skipping due to broken symlink" - echo " Removing broken symlink from vault: $META_SRC" - rm -f "$META_SRC" - fi + if [[ -z "$REL_PATH" ]]; then + TARGET_PARENT="$TARGET_DIR" + else + TARGET_PARENT="$TARGET_DIR/$REL_PATH" + fi + TARGET_META="$TARGET_PARENT/$pattern" + + echo " Found source: $META_SRC" + echo " -> Installing to: $TARGET_META" - count=$((count + 1)) - done < <(find -L "$SOURCE_DIR" \( -type l -o -type d \) -name "$pattern" -print0 2>/dev/null) + mkdir -p "$TARGET_PARENT" - if [[ $count -eq 0 ]]; then - echo " (no '$pattern' directories found in vault)" + if [[ -e "$TARGET_META" || -L "$TARGET_META" ]]; then + rm -rf "$TARGET_META" + fi + + if ! copy_real_metadata "$META_SRC" "$TARGET_META"; then + echo " Skipping due to broken symlink" + echo " Removing broken symlink from vault: $META_SRC" + rm -f "$META_SRC" fi -} -# Import each configured pattern -for pattern in $WT_METADATA_PATTERNS; do - import_pattern "$pattern" + count=$((count + 1)) +done < <(find_all_import_dirs) + +if [[ $count -eq 0 ]]; then + echo " (no metadata directories found in vault)" +else echo -done + echo "Imported $count metadata directories." +fi success "Done importing metadata." diff --git a/lib/wt-common b/lib/wt-common index 8cd19bc..cf4dc25 100644 --- a/lib/wt-common +++ b/lib/wt-common @@ -373,6 +373,31 @@ _wt_expand_path() { echo "${path/#\~\//$HOME/}" } +# Prune paths where an ancestor is already in the set. +# When metadata patterns nest (e.g., .ijwb contains .idea), only the +# outermost directory needs to be exported/imported — the children are +# already included. +# Reads newline-separated absolute paths from stdin, writes pruned paths to stdout. +_wt_prune_nested_paths() { + local sorted + sorted=$(sort) + local kept_paths=() + while IFS= read -r _entry_path; do + [[ -z "$_entry_path" ]] && continue + local dominated=false + for kept in ${kept_paths[@]+"${kept_paths[@]}"}; do + if [[ "$_entry_path" == "$kept/"* ]]; then + dominated=true + break + fi + done + if [[ "$dominated" == "false" ]]; then + kept_paths+=("$_entry_path") + echo "$_entry_path" + fi + done <<< "$sorted" +} + # Check if a path value is valid for a directory config variable. # Valid means: absolute path (starts with /) and contains no glob characters. # Args: $1 = path value diff --git a/lib/wt-context-setup b/lib/wt-context-setup index 05c893b..fc7621c 100644 --- a/lib/wt-context-setup +++ b/lib/wt-context-setup @@ -124,23 +124,10 @@ _wt_detect_metadata_patterns() { return fi - local sorted_paths - sorted_paths=$(printf '%s\n' "${all_paths[@]}" | sort) - local kept_paths=() while IFS= read -r _entry_path; do - [[ -z "$_entry_path" ]] && continue - local dominated=false - for kept in ${kept_paths[@]+"${kept_paths[@]}"}; do - if [[ "$_entry_path" == "$kept/"* ]]; then - dominated=true - break - fi - done - if [[ "$dominated" == "false" ]]; then - kept_paths+=("$_entry_path") - fi - done <<< "$sorted_paths" + [[ -n "$_entry_path" ]] && kept_paths+=("$_entry_path") + done < <(printf '%s\n' "${all_paths[@]}" | _wt_prune_nested_paths) local patterns=() for _entry_path in ${kept_paths[@]+"${kept_paths[@]}"}; do diff --git a/test/integration/wt-metadata-import.bats b/test/integration/wt-metadata-import.bats index f40ce20..ea9d241 100644 --- a/test/integration/wt-metadata-import.bats +++ b/test/integration/wt-metadata-import.bats @@ -258,3 +258,33 @@ teardown() { assert [ -f "$WORKTREE/.idea/config.xml" ] assert_equal "$(cat "$WORKTREE/.idea/config.xml")" "config content" } + +# ============================================================================= +# Nested metadata pruning tests +# ============================================================================= + +@test "wt-metadata-import does not import nested metadata inside parent pattern" { + sed -i.bak 's/WT_METADATA_PATTERNS=".*"/WT_METADATA_PATTERNS=".ijwb .idea"/' "$TEST_HOME/.wt/repos/test.conf" + + # Create .ijwb containing .idea in the real repo (standard Bazel plugin structure) + mkdir -p "$REPO/myproject/.ijwb/.idea" + echo "config" > "$REPO/myproject/.ijwb/config.xml" + echo "idea-config" > "$REPO/myproject/.ijwb/.idea/workspace.xml" + + # Export to vault first (creates symlinks, which is the real-world structure) + run "$TEST_HOME/.wt/bin/wt-metadata-export" -y + assert_success + # Vault should have a symlink for .ijwb, not a separate .idea entry + assert [ -L "$WT_IDEA_FILES_BASE/myproject/.ijwb" ] + + # Import into worktree + run "$TEST_HOME/.wt/bin/wt-metadata-import" -y "$WT_IDEA_FILES_BASE" "$WORKTREE" + assert_success + + # .ijwb should be imported (with .idea inside it via the copy) + assert [ -d "$WORKTREE/myproject/.ijwb" ] + assert [ -f "$WORKTREE/myproject/.ijwb/.idea/workspace.xml" ] + + # Output should NOT show a separate .idea import for the nested dir + refute_output --partial "myproject/.ijwb/.idea" +} diff --git a/test/unit/wt-common.bats b/test/unit/wt-common.bats index 0f953b5..0397a44 100644 --- a/test/unit/wt-common.bats +++ b/test/unit/wt-common.bats @@ -754,6 +754,35 @@ teardown() { assert_equal "$WT_BASE_BRANCH" "pre-existing" } +# ============================================================================= +# Tests for _wt_prune_nested_paths() +# ============================================================================= + +@test "_wt_prune_nested_paths removes nested paths" { + result=$(printf '%s\n' "/a/.ijwb" "/a/.ijwb/.idea" "/b/.idea" | _wt_prune_nested_paths) + assert_equal "$result" "$(printf '%s\n' "/a/.ijwb" "/b/.idea")" +} + +@test "_wt_prune_nested_paths keeps siblings" { + result=$(printf '%s\n' "/a/.idea" "/a/.ijwb" | _wt_prune_nested_paths) + assert_equal "$result" "$(printf '%s\n' "/a/.idea" "/a/.ijwb")" +} + +@test "_wt_prune_nested_paths handles empty input" { + result=$(printf '' | _wt_prune_nested_paths) + assert_equal "$result" "" +} + +@test "_wt_prune_nested_paths handles single path" { + result=$(printf '%s\n' "/a/.idea" | _wt_prune_nested_paths) + assert_equal "$result" "/a/.idea" +} + +@test "_wt_prune_nested_paths handles multi-level nesting" { + result=$(printf '%s\n' "/a/.ijwb" "/a/.ijwb/.idea" "/a/.ijwb/.idea/.run" | _wt_prune_nested_paths) + assert_equal "$result" "/a/.ijwb" +} + # ============================================================================= # Tests for _wt_is_valid_path_config() # =============================================================================