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