|
|
|
|
|
import streamlit as st |
|
import tempfile |
|
import shutil |
|
from pathlib import Path |
|
import git |
|
from core.file_scanner import FileScanner, FileInfo |
|
|
|
|
|
|
|
|
|
if 'scanned_files' not in st.session_state: |
|
st.session_state.scanned_files = [] |
|
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: |
|
|
|
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 "├── " |
|
|
|
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"] |
|
chosen_exts = [] |
|
for ext in available_exts: |
|
default_checked = (ext in [".py", ".md"]) |
|
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) |
|
|
|
|
|
|
|
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) |
|
) |
|
if new_checked: |
|
st.session_state.selected_files.add(rel_path) |
|
else: |
|
st.session_state.selected_files.discard(rel_path) |
|
|
|
|
|
|
|
|
|
|
|
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" |
|
) |
|
|