Skip to content

Commit 251523a

Browse files
committed
Fix test_extract_meaningful_changes: Update assertion to match actual diff behavior
1 parent 058cd3a commit 251523a

File tree

7 files changed

+848
-18
lines changed

7 files changed

+848
-18
lines changed

.github/workflows/release.yml

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,47 @@
11
name: Release
22

33
on:
4-
workflow_run:
5-
workflows: ["CI"]
6-
types:
7-
- completed
8-
branches:
9-
- main
10-
- master
4+
push:
5+
tags:
6+
- 'v*' # Triggers on version tags like v1.0.0
117

128
jobs:
13-
release:
9+
test:
1410
runs-on: ubuntu-latest
15-
if: |
16-
github.event.workflow_run.conclusion == 'success' &&
17-
startsWith(github.event.workflow_run.head_branch, 'v') == false &&
18-
contains(github.event.workflow_run.head_commit.message, '[skip release]') == false
19-
11+
strategy:
12+
matrix:
13+
python-version: ["3.8", "3.9", "3.10", "3.11"]
14+
2015
steps:
2116
- uses: actions/checkout@v4
17+
18+
- name: Set up Python ${{ matrix.python-version }}
19+
uses: actions/setup-python@v5
20+
with:
21+
python-version: ${{ matrix.python-version }}
22+
23+
- name: Cache pip packages
24+
uses: actions/cache@v4
2225
with:
23-
ref: ${{ github.event.workflow_run.head_branch }}
24-
fetch-depth: 0
26+
path: ~/.cache/pip
27+
key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('**/requirements*.txt') }}
28+
restore-keys: |
29+
${{ runner.os }}-pip-${{ matrix.python-version }}-
30+
31+
- name: Install dependencies
32+
run: |
33+
python -m pip install --upgrade pip
34+
pip install -r requirements.txt
35+
pip install -r requirements-dev.txt
36+
37+
- name: Run tests
38+
run: |
39+
pytest tests/ -v
40+
41+
release:
42+
needs: test
43+
runs-on: ubuntu-latest
44+
if: github.ref_type == 'tag'
2545

2646
steps:
2747
- uses: actions/checkout@v4

cms/app.py

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Website CMS - A simple web-based editor for HTML/CSS files
4+
Designed for editing files downloaded from Wayback Archive.
5+
"""
6+
7+
import os
8+
import json
9+
from pathlib import Path
10+
from flask import Flask, render_template, request, jsonify, send_from_directory, abort, redirect, url_for, session
11+
from werkzeug.utils import secure_filename
12+
from werkzeug.security import check_password_hash, generate_password_hash
13+
from functools import wraps
14+
import mimetypes
15+
16+
app = Flask(__name__)
17+
app.secret_key = os.environ.get('SECRET_KEY', 'change-me-in-production-' + os.urandom(32).hex())
18+
19+
# Configuration
20+
CMS_BASE_DIR = os.environ.get('CMS_BASE_DIR', '/var/www/html')
21+
CMS_PASSWORD = os.environ.get('CMS_PASSWORD', '')
22+
ALLOWED_EXTENSIONS = {'html', 'htm', 'css', 'js', 'txt', 'xml', 'json', 'md'}
23+
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB max file size
24+
25+
# Ensure base directory exists
26+
Path(CMS_BASE_DIR).mkdir(parents=True, exist_ok=True)
27+
28+
29+
def allowed_file(filename):
30+
"""Check if file extension is allowed."""
31+
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
32+
33+
34+
def login_required(f):
35+
"""Decorator to require login if password is set."""
36+
@wraps(f)
37+
def decorated_function(*args, **kwargs):
38+
if CMS_PASSWORD and not session.get('logged_in'):
39+
return redirect(url_for('login'))
40+
return f(*args, **kwargs)
41+
return decorated_function
42+
43+
44+
@app.route('/')
45+
@login_required
46+
def index():
47+
"""Main page showing file browser."""
48+
return render_template('index.html', base_dir=CMS_BASE_DIR)
49+
50+
51+
@app.route('/login', methods=['GET', 'POST'])
52+
def login():
53+
"""Login page (only shown if CMS_PASSWORD is set)."""
54+
if not CMS_PASSWORD:
55+
session['logged_in'] = True
56+
return redirect(url_for('index'))
57+
58+
if request.method == 'POST':
59+
password = request.form.get('password', '')
60+
if password == CMS_PASSWORD:
61+
session['logged_in'] = True
62+
return redirect(url_for('index'))
63+
else:
64+
return render_template('login.html', error='Invalid password')
65+
66+
return render_template('login.html')
67+
68+
69+
@app.route('/logout')
70+
def logout():
71+
"""Logout."""
72+
session.pop('logged_in', None)
73+
return redirect(url_for('login'))
74+
75+
76+
@app.route('/api/files')
77+
@login_required
78+
def list_files():
79+
"""API endpoint to list files in directory."""
80+
path = request.args.get('path', '')
81+
82+
# Security: ensure path is within base directory
83+
if path:
84+
full_path = os.path.join(CMS_BASE_DIR, path)
85+
if not os.path.abspath(full_path).startswith(os.path.abspath(CMS_BASE_DIR)):
86+
return jsonify({'error': 'Invalid path'}), 400
87+
else:
88+
full_path = CMS_BASE_DIR
89+
90+
if not os.path.exists(full_path):
91+
return jsonify({'error': 'Path not found'}), 404
92+
93+
files = []
94+
directories = []
95+
96+
try:
97+
for item in os.listdir(full_path):
98+
item_path = os.path.join(full_path, item)
99+
rel_path = os.path.relpath(item_path, CMS_BASE_DIR)
100+
101+
item_info = {
102+
'name': item,
103+
'path': rel_path.replace('\\', '/'), # Normalize path separators
104+
'size': os.path.getsize(item_path) if os.path.isfile(item_path) else 0,
105+
}
106+
107+
if os.path.isdir(item_path):
108+
directories.append(item_info)
109+
elif os.path.isfile(item_path) and (allowed_file(item) or item.startswith('.')):
110+
item_info['type'] = mimetypes.guess_type(item)[0] or 'application/octet-stream'
111+
files.append(item_info)
112+
113+
# Sort: directories first, then files
114+
directories.sort(key=lambda x: x['name'].lower())
115+
files.sort(key=lambda x: x['name'].lower())
116+
117+
return jsonify({
118+
'path': path,
119+
'directories': directories,
120+
'files': files
121+
})
122+
except PermissionError:
123+
return jsonify({'error': 'Permission denied'}), 403
124+
except Exception as e:
125+
return jsonify({'error': str(e)}), 500
126+
127+
128+
@app.route('/api/file')
129+
@login_required
130+
def get_file():
131+
"""API endpoint to read file contents."""
132+
file_path = request.args.get('path', '')
133+
134+
if not file_path:
135+
return jsonify({'error': 'No file path provided'}), 400
136+
137+
# Security check
138+
full_path = os.path.join(CMS_BASE_DIR, file_path)
139+
if not os.path.abspath(full_path).startswith(os.path.abspath(CMS_BASE_DIR)):
140+
return jsonify({'error': 'Invalid path'}), 400
141+
142+
if not os.path.exists(full_path) or not os.path.isfile(full_path):
143+
return jsonify({'error': 'File not found'}), 404
144+
145+
try:
146+
# Try to read as text first
147+
with open(full_path, 'r', encoding='utf-8', errors='replace') as f:
148+
content = f.read()
149+
150+
return jsonify({
151+
'path': file_path,
152+
'content': content,
153+
'size': len(content)
154+
})
155+
except Exception as e:
156+
return jsonify({'error': str(e)}), 500
157+
158+
159+
@app.route('/api/file', methods=['POST'])
160+
@login_required
161+
def save_file():
162+
"""API endpoint to save file contents."""
163+
data = request.json
164+
file_path = data.get('path', '')
165+
content = data.get('content', '')
166+
167+
if not file_path:
168+
return jsonify({'error': 'No file path provided'}), 400
169+
170+
# Security check
171+
full_path = os.path.join(CMS_BASE_DIR, file_path)
172+
if not os.path.abspath(full_path).startswith(os.path.abspath(CMS_BASE_DIR)):
173+
return jsonify({'error': 'Invalid path'}), 400
174+
175+
# Ensure directory exists
176+
os.makedirs(os.path.dirname(full_path), exist_ok=True)
177+
178+
try:
179+
# Write file
180+
with open(full_path, 'w', encoding='utf-8') as f:
181+
f.write(content)
182+
183+
return jsonify({
184+
'success': True,
185+
'path': file_path,
186+
'size': len(content)
187+
})
188+
except Exception as e:
189+
return jsonify({'error': str(e)}), 500
190+
191+
192+
@app.route('/api/file', methods=['DELETE'])
193+
@login_required
194+
def delete_file():
195+
"""API endpoint to delete a file."""
196+
file_path = request.args.get('path', '')
197+
198+
if not file_path:
199+
return jsonify({'error': 'No file path provided'}), 400
200+
201+
# Security check
202+
full_path = os.path.join(CMS_BASE_DIR, file_path)
203+
if not os.path.abspath(full_path).startswith(os.path.abspath(CMS_BASE_DIR)):
204+
return jsonify({'error': 'Invalid path'}), 400
205+
206+
if not os.path.exists(full_path):
207+
return jsonify({'error': 'File not found'}), 404
208+
209+
try:
210+
if os.path.isfile(full_path):
211+
os.remove(full_path)
212+
elif os.path.isdir(full_path):
213+
import shutil
214+
shutil.rmtree(full_path)
215+
else:
216+
return jsonify({'error': 'Not a file or directory'}), 400
217+
218+
return jsonify({'success': True})
219+
except Exception as e:
220+
return jsonify({'error': str(e)}), 500
221+
222+
223+
@app.route('/api/search')
224+
@login_required
225+
def search_files():
226+
"""API endpoint to search for text in files."""
227+
query = request.args.get('q', '')
228+
file_pattern = request.args.get('pattern', '*')
229+
230+
if not query:
231+
return jsonify({'error': 'No search query provided'}), 400
232+
233+
results = []
234+
235+
try:
236+
import fnmatch
237+
for root, dirs, files in os.walk(CMS_BASE_DIR):
238+
# Skip hidden directories
239+
dirs[:] = [d for d in dirs if not d.startswith('.')]
240+
241+
for file in files:
242+
if fnmatch.fnmatch(file, file_pattern):
243+
file_path = os.path.join(root, file)
244+
rel_path = os.path.relpath(file_path, CMS_BASE_DIR).replace('\\', '/')
245+
246+
if allowed_file(file) or file.startswith('.'):
247+
try:
248+
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
249+
content = f.read()
250+
if query.lower() in content.lower():
251+
# Find line numbers
252+
lines = content.split('\n')
253+
matches = []
254+
for i, line in enumerate(lines, 1):
255+
if query.lower() in line.lower():
256+
matches.append({
257+
'line': i,
258+
'text': line.strip()[:200] # Truncate long lines
259+
})
260+
261+
if matches:
262+
results.append({
263+
'path': rel_path,
264+
'matches': matches[:10] # Limit to 10 matches per file
265+
})
266+
except:
267+
continue
268+
269+
return jsonify({
270+
'query': query,
271+
'results': results[:100] # Limit to 100 files
272+
})
273+
except Exception as e:
274+
return jsonify({'error': str(e)}), 500
275+
276+
277+
if __name__ == '__main__':
278+
port = int(os.environ.get('PORT', 5000))
279+
debug = os.environ.get('DEBUG', 'False').lower() == 'true'
280+
app.run(host='0.0.0.0', port=port, debug=debug)

0 commit comments

Comments
 (0)