Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -7,109 +7,181 @@ from pathlib import Path
|
|
7 |
import git # GitPython
|
8 |
from core.file_scanner import FileScanner, FileInfo
|
9 |
|
10 |
-
|
11 |
# セッション状態の初期化
|
|
|
12 |
if 'scanned_files' not in st.session_state:
|
13 |
st.session_state.scanned_files = [] # スキャンしたFileInfoリスト
|
14 |
if 'selected_files' not in st.session_state:
|
15 |
-
st.session_state.selected_files = set() # ユーザーが選択中のファイルパス
|
16 |
if 'cloned_repo_dir' not in st.session_state:
|
17 |
-
st.session_state.cloned_repo_dir = None #
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
18 |
|
19 |
-
st.title("Gitリポジトリ スキャナー (削除ボタン付き)")
|
20 |
|
21 |
-
#
|
22 |
-
|
|
|
|
|
23 |
|
24 |
-
# --- 2) スキャン対象拡張子の選択 ---
|
25 |
st.subheader("スキャン対象拡張子")
|
26 |
-
available_exts = [".py", ".js", ".ts", ".md", ".txt", ".java", ".cpp"
|
27 |
chosen_exts = []
|
28 |
for ext in available_exts:
|
29 |
-
default_checked = (ext in [".py", ".md"]) #
|
30 |
if st.checkbox(ext, key=f"ext_{ext}", value=default_checked):
|
31 |
chosen_exts.append(ext)
|
32 |
|
33 |
-
#
|
|
|
|
|
34 |
if st.button("スキャン開始"):
|
35 |
if not repo_url.strip():
|
36 |
-
st.error("リポジトリURL
|
37 |
else:
|
38 |
-
#
|
39 |
if st.session_state.cloned_repo_dir and Path(st.session_state.cloned_repo_dir).exists():
|
40 |
shutil.rmtree(st.session_state.cloned_repo_dir, ignore_errors=True)
|
41 |
|
42 |
-
#
|
43 |
tmp_dir = tempfile.mkdtemp()
|
44 |
clone_path = Path(tmp_dir) / "cloned_repo"
|
45 |
|
46 |
-
st.write(f"リポジトリを一時ディレクトリへクローン中: {clone_path}")
|
47 |
try:
|
|
|
48 |
git.Repo.clone_from(repo_url, clone_path)
|
49 |
st.session_state.cloned_repo_dir = str(clone_path)
|
50 |
except Exception as e:
|
51 |
st.error(f"クローン失敗: {e}")
|
|
|
|
|
52 |
st.stop()
|
53 |
-
|
54 |
# スキャン
|
55 |
scanner = FileScanner(base_dir=clone_path, target_extensions=set(chosen_exts))
|
56 |
found_files = scanner.scan_files()
|
57 |
|
58 |
-
# スキャン結果をセッションに保存
|
59 |
st.session_state.scanned_files = found_files
|
60 |
st.session_state.selected_files = set()
|
61 |
|
62 |
st.success(f"スキャン完了: {len(found_files)}個のファイルを検出")
|
63 |
|
64 |
-
#
|
|
|
|
|
65 |
if st.session_state.cloned_repo_dir:
|
66 |
-
|
67 |
-
if st.button("クローン済みリポジトリを削除"):
|
68 |
-
# クローン先フォルダを削除
|
69 |
shutil.rmtree(st.session_state.cloned_repo_dir, ignore_errors=True)
|
70 |
st.session_state.cloned_repo_dir = None
|
71 |
st.session_state.scanned_files = []
|
72 |
st.session_state.selected_files = set()
|
73 |
-
st.success("
|
74 |
-
|
75 |
|
76 |
-
#
|
|
|
|
|
77 |
if st.session_state.scanned_files:
|
78 |
-
st.write("## スキャン結果 (指定拡張子のみ)")
|
79 |
-
|
80 |
-
col1, col2 = st.columns(2)
|
81 |
-
with col1:
|
82 |
-
if st.button("すべて選択"):
|
83 |
-
base_path = Path(st.session_state.cloned_repo_dir)
|
84 |
-
st.session_state.selected_files = {
|
85 |
-
f.path.relative_to(base_path) for f in st.session_state.scanned_files
|
86 |
-
}
|
87 |
-
with col2:
|
88 |
-
if st.button("すべて解除"):
|
89 |
-
st.session_state.selected_files = set()
|
90 |
-
|
91 |
base_path = Path(st.session_state.cloned_repo_dir)
|
92 |
-
for file_info in st.session_state.scanned_files:
|
93 |
-
rel_path = file_info.path.relative_to(base_path)
|
94 |
-
checked = (rel_path in st.session_state.selected_files)
|
95 |
-
|
96 |
-
# ファイルのチェックボックスを表示
|
97 |
-
new_checked = st.checkbox(
|
98 |
-
f"{rel_path} ({file_info.formatted_size})",
|
99 |
-
value=checked,
|
100 |
-
key=str(rel_path) # key 重複回避のため文字列化
|
101 |
-
)
|
102 |
-
if new_checked:
|
103 |
-
st.session_state.selected_files.add(rel_path)
|
104 |
-
else:
|
105 |
-
st.session_state.selected_files.discard(rel_path)
|
106 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
107 |
|
108 |
-
|
|
|
|
|
|
|
109 |
def create_markdown_for_selected(files, selected_paths, base_dir: Path) -> str:
|
110 |
-
"""
|
111 |
-
選択さ���たファイルを1つのMarkdownとして連結。
|
112 |
-
"""
|
113 |
output = []
|
114 |
for f in files:
|
115 |
rel_path = f.path.relative_to(base_dir)
|
@@ -120,20 +192,18 @@ def create_markdown_for_selected(files, selected_paths, base_dir: Path) -> str:
|
|
120 |
output.append(f.content)
|
121 |
else:
|
122 |
output.append("# Failed to read content")
|
123 |
-
output.append("") #
|
124 |
-
|
125 |
return "\n".join(output)
|
126 |
|
127 |
if st.session_state.scanned_files:
|
128 |
-
st.write("##
|
129 |
-
if st.button("
|
130 |
base_path = Path(st.session_state.cloned_repo_dir)
|
131 |
markdown_text = create_markdown_for_selected(
|
132 |
st.session_state.scanned_files,
|
133 |
st.session_state.selected_files,
|
134 |
base_path
|
135 |
)
|
136 |
-
# ダウンロードボタン
|
137 |
st.download_button(
|
138 |
label="Markdownダウンロード",
|
139 |
data=markdown_text,
|
|
|
7 |
import git # GitPython
|
8 |
from core.file_scanner import FileScanner, FileInfo
|
9 |
|
10 |
+
# =====================================
|
11 |
# セッション状態の初期化
|
12 |
+
# =====================================
|
13 |
if 'scanned_files' not in st.session_state:
|
14 |
st.session_state.scanned_files = [] # スキャンしたFileInfoリスト
|
15 |
if 'selected_files' not in st.session_state:
|
16 |
+
st.session_state.selected_files = set() # ユーザーが選択中のファイルパス (相対パス)
|
17 |
if 'cloned_repo_dir' not in st.session_state:
|
18 |
+
st.session_state.cloned_repo_dir = None # クローン先ディレクトリの絶対パス文字列
|
19 |
+
|
20 |
+
# =====================================
|
21 |
+
# タイトル等
|
22 |
+
# =====================================
|
23 |
+
st.title("Gitリポジトリ スキャナー")
|
24 |
+
st.markdown("**ディレクトリ構造をツリー表示し、ファイルを選んでMarkdownダウンロードできます**")
|
25 |
+
|
26 |
+
# =====================================
|
27 |
+
# ツリー構造を生成する関数
|
28 |
+
# =====================================
|
29 |
+
def build_tree(paths):
|
30 |
+
"""
|
31 |
+
相対パス(Pathオブジェクト)のリストからツリー状のネスト構造を構築する。
|
32 |
+
戻り値は {要素名 -> 子要素のdict or None} という入れ子の辞書。
|
33 |
+
"""
|
34 |
+
tree = {}
|
35 |
+
for p in paths:
|
36 |
+
parts = p.parts
|
37 |
+
current = tree
|
38 |
+
for i, part in enumerate(parts):
|
39 |
+
if i == len(parts) - 1:
|
40 |
+
# ファイルやフォルダの末端
|
41 |
+
current[part] = None
|
42 |
+
else:
|
43 |
+
if part not in current:
|
44 |
+
current[part] = {}
|
45 |
+
if isinstance(current[part], dict):
|
46 |
+
current = current[part]
|
47 |
+
else:
|
48 |
+
# もしNoneだった場合(同名のファイル/フォルダがあるなど) → 無理やりdictに
|
49 |
+
current[part] = {}
|
50 |
+
current = current[part]
|
51 |
+
return tree
|
52 |
+
|
53 |
+
def format_tree(tree_dict, prefix=""):
|
54 |
+
"""
|
55 |
+
build_tree()で作ったネスト構造をASCIIアートのツリー文字列にする。
|
56 |
+
"""
|
57 |
+
lines = []
|
58 |
+
entries = sorted(tree_dict.keys())
|
59 |
+
for i, entry in enumerate(entries):
|
60 |
+
is_last = (i == len(entries) - 1)
|
61 |
+
marker = "└── " if is_last else "├── "
|
62 |
+
# 子要素がある(=dict)ならフォルダ、Noneならファイル
|
63 |
+
if isinstance(tree_dict[entry], dict):
|
64 |
+
# フォルダとして表示
|
65 |
+
lines.append(prefix + marker + entry + "/")
|
66 |
+
# 次の階層のプレフィックスを用意
|
67 |
+
extension = " " if is_last else "│ "
|
68 |
+
sub_prefix = prefix + extension
|
69 |
+
# 再帰的に生成
|
70 |
+
lines.extend(format_tree(tree_dict[entry], sub_prefix))
|
71 |
+
else:
|
72 |
+
# ファイルとして表示
|
73 |
+
lines.append(prefix + marker + entry)
|
74 |
+
return lines
|
75 |
|
|
|
76 |
|
77 |
+
# =====================================
|
78 |
+
# ユーザー入力
|
79 |
+
# =====================================
|
80 |
+
repo_url = st.text_input("GitリポジトリURL (例: https://github.com/username/repo.git)")
|
81 |
|
|
|
82 |
st.subheader("スキャン対象拡張子")
|
83 |
+
available_exts = [".py", ".js", ".ts", ".md", ".txt", ".java", ".cpp"]
|
84 |
chosen_exts = []
|
85 |
for ext in available_exts:
|
86 |
+
default_checked = (ext in [".py", ".md"]) # デモ用に .py と .md を初期ON
|
87 |
if st.checkbox(ext, key=f"ext_{ext}", value=default_checked):
|
88 |
chosen_exts.append(ext)
|
89 |
|
90 |
+
# =====================================
|
91 |
+
# スキャン開始ボタン
|
92 |
+
# =====================================
|
93 |
if st.button("スキャン開始"):
|
94 |
if not repo_url.strip():
|
95 |
+
st.error("リポジトリURLを入力してください。")
|
96 |
else:
|
97 |
+
# 既にクローン済フォルダがあれば削除
|
98 |
if st.session_state.cloned_repo_dir and Path(st.session_state.cloned_repo_dir).exists():
|
99 |
shutil.rmtree(st.session_state.cloned_repo_dir, ignore_errors=True)
|
100 |
|
101 |
+
# 一時フォルダを作成してクローン
|
102 |
tmp_dir = tempfile.mkdtemp()
|
103 |
clone_path = Path(tmp_dir) / "cloned_repo"
|
104 |
|
|
|
105 |
try:
|
106 |
+
st.write(f"リポジトリをクローン中: {clone_path}")
|
107 |
git.Repo.clone_from(repo_url, clone_path)
|
108 |
st.session_state.cloned_repo_dir = str(clone_path)
|
109 |
except Exception as e:
|
110 |
st.error(f"クローン失敗: {e}")
|
111 |
+
st.session_state.cloned_repo_dir = None
|
112 |
+
st.session_state.scanned_files = []
|
113 |
st.stop()
|
114 |
+
|
115 |
# スキャン
|
116 |
scanner = FileScanner(base_dir=clone_path, target_extensions=set(chosen_exts))
|
117 |
found_files = scanner.scan_files()
|
118 |
|
|
|
119 |
st.session_state.scanned_files = found_files
|
120 |
st.session_state.selected_files = set()
|
121 |
|
122 |
st.success(f"スキャン完了: {len(found_files)}個のファイルを検出")
|
123 |
|
124 |
+
# =====================================
|
125 |
+
# クローン削除ボタン
|
126 |
+
# =====================================
|
127 |
if st.session_state.cloned_repo_dir:
|
128 |
+
if st.button("クローン済みデータを削除"):
|
|
|
|
|
129 |
shutil.rmtree(st.session_state.cloned_repo_dir, ignore_errors=True)
|
130 |
st.session_state.cloned_repo_dir = None
|
131 |
st.session_state.scanned_files = []
|
132 |
st.session_state.selected_files = set()
|
133 |
+
st.success("クローンしたディレクトリを削除しました")
|
|
|
134 |
|
135 |
+
# =====================================
|
136 |
+
# スキャン結果がある場合 → ツリー表示 + ファイル選択
|
137 |
+
# =====================================
|
138 |
if st.session_state.scanned_files:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
139 |
base_path = Path(st.session_state.cloned_repo_dir)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
140 |
|
141 |
+
# --- ツリーを作る ---
|
142 |
+
# scanned_files は「指定拡張子」だけ取得されているので、そのファイルパスのみでツリーを構築
|
143 |
+
rel_paths = [f.path.relative_to(base_path) for f in st.session_state.scanned_files]
|
144 |
+
tree_dict = build_tree(rel_paths)
|
145 |
+
tree_lines = format_tree(tree_dict)
|
146 |
+
ascii_tree = "\n".join(tree_lines)
|
147 |
+
|
148 |
+
st.write("## スキャン結果")
|
149 |
+
col_tree, col_files = st.columns([1, 2]) # 左:ツリー, 右:ファイル一覧
|
150 |
+
|
151 |
+
with col_tree:
|
152 |
+
st.markdown("**ディレクトリ構造 (指定拡張子のみ)**")
|
153 |
+
st.markdown(f"```\n{ascii_tree}\n```")
|
154 |
+
|
155 |
+
with col_files:
|
156 |
+
st.markdown("**ファイル一覧 (チェックボックス)**")
|
157 |
+
|
158 |
+
col_btn1, col_btn2 = st.columns(2)
|
159 |
+
with col_btn1:
|
160 |
+
if st.button("すべて選択"):
|
161 |
+
st.session_state.selected_files = set(rel_paths)
|
162 |
+
with col_btn2:
|
163 |
+
if st.button("すべて解除"):
|
164 |
+
st.session_state.selected_files = set()
|
165 |
+
|
166 |
+
for file_info in st.session_state.scanned_files:
|
167 |
+
rel_path = file_info.path.relative_to(base_path)
|
168 |
+
checked = rel_path in st.session_state.selected_files
|
169 |
+
|
170 |
+
new_checked = st.checkbox(
|
171 |
+
f"{rel_path} ({file_info.formatted_size})",
|
172 |
+
value=checked,
|
173 |
+
key=str(rel_path) # keyの重複回避
|
174 |
+
)
|
175 |
+
if new_checked:
|
176 |
+
st.session_state.selected_files.add(rel_path)
|
177 |
+
else:
|
178 |
+
st.session_state.selected_files.discard(rel_path)
|
179 |
|
180 |
+
|
181 |
+
# =====================================
|
182 |
+
# 選択ファイルをまとめてMarkdown化 & ダウンロード
|
183 |
+
# =====================================
|
184 |
def create_markdown_for_selected(files, selected_paths, base_dir: Path) -> str:
|
|
|
|
|
|
|
185 |
output = []
|
186 |
for f in files:
|
187 |
rel_path = f.path.relative_to(base_dir)
|
|
|
192 |
output.append(f.content)
|
193 |
else:
|
194 |
output.append("# Failed to read content")
|
195 |
+
output.append("") # 空行
|
|
|
196 |
return "\n".join(output)
|
197 |
|
198 |
if st.session_state.scanned_files:
|
199 |
+
st.write("## 選択ファイルをダウンロード")
|
200 |
+
if st.button("選択ファイルをMarkdownとしてダウンロード"):
|
201 |
base_path = Path(st.session_state.cloned_repo_dir)
|
202 |
markdown_text = create_markdown_for_selected(
|
203 |
st.session_state.scanned_files,
|
204 |
st.session_state.selected_files,
|
205 |
base_path
|
206 |
)
|
|
|
207 |
st.download_button(
|
208 |
label="Markdownダウンロード",
|
209 |
data=markdown_text,
|