Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 2 additions & 25 deletions bin/wt-metadata-export
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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..."
Expand Down
102 changes: 54 additions & 48 deletions bin/wt-metadata-import
Original file line number Diff line number Diff line change
Expand Up @@ -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."
25 changes: 25 additions & 0 deletions lib/wt-common
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 2 additions & 15 deletions lib/wt-context-setup
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 30 additions & 0 deletions test/integration/wt-metadata-import.bats
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
29 changes: 29 additions & 0 deletions test/unit/wt-common.bats
Original file line number Diff line number Diff line change
Expand Up @@ -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()
# =============================================================================
Expand Down