Spaces:
Sleeping
Sleeping
| # 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" | |
| ) | |