# 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" )