File size: 8,644 Bytes
b7b8cc1
 
227e75d
 
b7b8cc1
227e75d
b7b8cc1
396927c
31a278f
b7b8cc1
227e75d
b7b8cc1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
749e7c0
b7b8cc1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0520d98
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# app.py

import streamlit as st
import tempfile
import shutil
from pathlib import Path
import git  # GitPython
from core.file_scanner import FileScanner, FileInfo

# =====================================
# セッション状態の初期化
# =====================================
if 'scanned_files' not in st.session_state:
    st.session_state.scanned_files = []  # スキャンしたFileInfoリスト
if 'selected_files' not in st.session_state:
    st.session_state.selected_files = set()  # ユーザーが選択中のファイルパス (相対パス)
if 'cloned_repo_dir' not in st.session_state:
    st.session_state.cloned_repo_dir = None  # クローン先ディレクトリの絶対パス文字列

# =====================================
# タイトル等
# =====================================
st.title("Gitリポジトリ スキャナー")
st.markdown("**ディレクトリ構造をツリー表示し、ファイルを選んでMarkdownダウンロードできます**\n(**ワイドモード推奨**)")

# =====================================
# ツリー構造を生成する関数
# =====================================
def build_tree(paths):
    """
    相対パス(Pathオブジェクト)のリストからツリー状のネスト構造を構築する。
    戻り値は {要素名 -> 子要素のdict or None} という入れ子の辞書。
    """
    tree = {}
    for p in paths:
        parts = p.parts
        current = tree
        for i, part in enumerate(parts):
            if i == len(parts) - 1:
                # ファイルやフォルダの末端
                current[part] = None
            else:
                if part not in current:
                    current[part] = {}
                if isinstance(current[part], dict):
                    current = current[part]
                else:
                    # もしNoneだった場合(同名のファイル/フォルダがあるなど) → 無理やりdictに
                    current[part] = {}
                    current = current[part]
    return tree

def format_tree(tree_dict, prefix=""):
    """
    build_tree()で作ったネスト構造をASCIIアートのツリー文字列にする。
    """
    lines = []
    entries = sorted(tree_dict.keys())
    for i, entry in enumerate(entries):
        is_last = (i == len(entries) - 1)
        marker = "└── " if is_last else "├── "
        # 子要素がある(=dict)ならフォルダ、Noneならファイル
        if isinstance(tree_dict[entry], dict):
            # フォルダとして表示
            lines.append(prefix + marker + entry + "/")
            # 次の階層のプレフィックスを用意
            extension = "    " if is_last else "│   "
            sub_prefix = prefix + extension
            # 再帰的に生成
            lines.extend(format_tree(tree_dict[entry], sub_prefix))
        else:
            # ファイルとして表示
            lines.append(prefix + marker + entry)
    return lines


# =====================================
# ユーザー入力
# =====================================
repo_url = st.text_input("GitリポジトリURL (例: https://github.com/username/repo.git)")

st.subheader("スキャン対象拡張子")
available_exts = [".py", ".js", ".ts", ".sh", ".md", ".txt", ".java", ".cpp", ".json",".yaml",""]
chosen_exts = []
for ext in available_exts:
    default_checked = (ext in [".py", ".md"])  # デモ用に .py と .md を初期ON
    if st.checkbox(ext, key=f"ext_{ext}", value=default_checked):
        chosen_exts.append(ext)

# =====================================
# スキャン開始ボタン
# =====================================
if st.button("スキャン開始"):
    if not repo_url.strip():
        st.error("リポジトリURLを入力してください。")
    else:
        # 既にクローン済フォルダがあれば削除
        if st.session_state.cloned_repo_dir and Path(st.session_state.cloned_repo_dir).exists():
            shutil.rmtree(st.session_state.cloned_repo_dir, ignore_errors=True)
        
        # 一時フォルダを作成してクローン
        tmp_dir = tempfile.mkdtemp()
        clone_path = Path(tmp_dir) / "cloned_repo"

        try:
            st.write(f"リポジトリをクローン中: {clone_path}")
            git.Repo.clone_from(repo_url, clone_path)
            st.session_state.cloned_repo_dir = str(clone_path)
        except Exception as e:
            st.error(f"クローン失敗: {e}")
            st.session_state.cloned_repo_dir = None
            st.session_state.scanned_files = []
            st.stop()

        # スキャン
        scanner = FileScanner(base_dir=clone_path, target_extensions=set(chosen_exts))
        found_files = scanner.scan_files()

        st.session_state.scanned_files = found_files
        st.session_state.selected_files = set()

        st.success(f"スキャン完了: {len(found_files)}個のファイルを検出")

# =====================================
# クローン削除ボタン
# =====================================
if st.session_state.cloned_repo_dir:
    if st.button("クローン済みデータを削除"):
        shutil.rmtree(st.session_state.cloned_repo_dir, ignore_errors=True)
        st.session_state.cloned_repo_dir = None
        st.session_state.scanned_files = []
        st.session_state.selected_files = set()
        st.success("クローンしたディレクトリを削除しました")

# =====================================
# スキャン結果がある場合 → ツリー表示 + ファイル選択
# =====================================
if st.session_state.scanned_files:
    base_path = Path(st.session_state.cloned_repo_dir)

    # --- ツリーを作る ---
    # scanned_files は「指定拡張子」だけ取得されているので、そのファイルパスのみでツリーを構築
    rel_paths = [f.path.relative_to(base_path) for f in st.session_state.scanned_files]
    tree_dict = build_tree(rel_paths)
    tree_lines = format_tree(tree_dict)
    ascii_tree = "\n".join(tree_lines)

    st.write("## スキャン結果")
    col_tree, col_files = st.columns([1, 2])  # 左:ツリー, 右:ファイル一覧

    with col_tree:
        st.markdown("**ディレクトリ構造 (指定拡張子のみ)**")
        st.markdown(f"```\n{ascii_tree}\n```")

    with col_files:
        st.markdown("**ファイル一覧 (チェックボックス)**")

        col_btn1, col_btn2 = st.columns(2)
        with col_btn1:
            if st.button("すべて選択"):
                st.session_state.selected_files = set(rel_paths)
        with col_btn2:
            if st.button("すべて解除"):
                st.session_state.selected_files = set()

        for file_info in st.session_state.scanned_files:
            rel_path = file_info.path.relative_to(base_path)
            checked = rel_path in st.session_state.selected_files

            new_checked = st.checkbox(
                f"{rel_path} ({file_info.formatted_size})",
                value=checked,
                key=str(rel_path)  # keyの重複回避
            )
            if new_checked:
                st.session_state.selected_files.add(rel_path)
            else:
                st.session_state.selected_files.discard(rel_path)


# =====================================
# 選択ファイルをまとめてMarkdown化 & ダウンロード
# =====================================
def create_markdown_for_selected(files, selected_paths, base_dir: Path) -> str:
    output = []
    for f in files:
        rel_path = f.path.relative_to(base_dir)
        if rel_path in selected_paths:
            output.append(f"## {rel_path}")
            output.append("------------")
            if f.content is not None:
                output.append(f.content)
            else:
                output.append("# Failed to read content")
            output.append("")  # 空行
    return "\n".join(output)

if st.session_state.scanned_files:
    st.write("## 選択ファイルをダウンロード")
    if st.button("選択ファイルをMarkdownとしてダウンロード(整形後,下にダウンロードボタンが出ます)"):
        base_path = Path(st.session_state.cloned_repo_dir)
        markdown_text = create_markdown_for_selected(
            st.session_state.scanned_files,
            st.session_state.selected_files,
            base_path
        )
        st.download_button(
            label="Markdownダウンロード",
            data=markdown_text,
            file_name="selected_files.md",
            mime="text/markdown"
        )