From 1343aaf7d036d6fb429440e178a03eac6824cc2f Mon Sep 17 00:00:00 2001 From: zPlus Date: Sun, 26 Feb 2023 23:49:35 +0100 Subject: [PATCH] Replace blank lines with empty lines. This was actually done automatically by the text editor since I changed its configuration. --- web.py | 318 ++++++++++++++++++++++++++++----------------------------- 1 file changed, 159 insertions(+), 159 deletions(-) diff --git a/web.py b/web.py index b143408..bb701cd 100644 --- a/web.py +++ b/web.py @@ -71,9 +71,9 @@ def list_repositories(): """ Scan GITOLITE_REPOSITORIES_ROOT for Git repositories, and return a list of them. """ - + repositories = [] - + # When topdown is True, the caller can modify the dirnames list in-place and # walk() will only recurse into the subdirectories whose names remain in dirnames; # this can be used to prune the search. @@ -81,25 +81,25 @@ def list_repositories(): for path, dirs, files in os.walk(GITOLITE_REPOSITORIES_ROOT, topdown=True): # Remove all files, we only want to recurse into directories files.clear() - + # This path is a git repo. Remove all sub-dirs because we don't need to # recurse any further if path.endswith('.git'): dirs.clear() - + repository = os.path.relpath(path, GITOLITE_REPOSITORIES_ROOT) - + # DO NOT LIST gitolite-admin repository! # This is the administration repository of this instance! if repository.lower() == 'gitolite-admin.git': continue - + try: with open(os.path.join(path, 'description')) as f: description = f.read() except: description = '' - + repositories.append({ 'path': repository, 'description': description @@ -112,15 +112,15 @@ def parse_thread_tags(data): """ Parse "tags" file of a mailing list thread. """ - + tags = {} - + for line in data.splitlines(): k, v = line.split('=', 1) k = k.strip() v = v.strip() tags[k] = tags.get(k, []) + [ v ] - + return tags @@ -146,11 +146,11 @@ def human_size(bytes, B=False): Convert a file size in bytes to a human friendly form. This is only used in templates when showing file sizes. """ - + for unit in [ 'B' if B else '', 'K', 'M', 'G', 'T', 'P' ]: if bytes < 1024: break bytes = bytes / 1024 - + return '{}{}'.format(round(bytes), unit).rjust(5) def humanct(commit_time, commit_time_offset = 0): @@ -158,12 +158,12 @@ def humanct(commit_time, commit_time_offset = 0): The following will add custom functions to the jinja2 template engine. These will be available to use within templates. """ - + delta = datetime.timedelta(minutes=commit_time_offset) tz = datetime.timezone(delta) dt = datetime.datetime.fromtimestamp(commit_time, tz) - + return dt.astimezone(pytz.utc).strftime('%Y-%m-%d %H:%M:%S') template = functools.partial(template, template_settings = { @@ -197,10 +197,10 @@ template = functools.partial(template, template_settings = { def error404(error): """ Custom 404 page. - + :param error: bottle.HTTPError given by Bottle when calling abort(404). """ - + return '[404] {}'.format(error.body) @bottle.get('/static/', name='static') @@ -216,7 +216,7 @@ def explore(): """ The home page displayed at https://domain/ """ - + repositories = list_repositories() return template('explore.html', repositories=repositories) @@ -225,29 +225,29 @@ def about(): """ The home page displayed at https://domain/ """ - + return template('about.html', domain=INSTANCE_DOMAIN) @bottle.get('/.git', name='overview') def overview(repository): """ Show README and other info about the repository. - + :param repository: Match repository name ending with ".git" """ - + repository += '.git' path = os.path.join(GITOLITE_REPOSITORIES_ROOT, repository) if not os.path.isdir(path): bottle.abort(404, 'No repository at this path.') - + repo = pygit2.Repository(path) local_branches = list(repo.branches.local) - + HEAD = None ref_name = None - + try: HEAD = repo.head.name ref_name = HEAD @@ -256,31 +256,31 @@ def overview(repository): if name_candidate in local_branches: ref_name = name_candidate break - + readme = '' - + if ref_name: tree = repo.revparse_single(ref_name).tree - + for e in tree: if e.name.lower() not in [ 'readme', 'readme.md', 'readme.rst' ]: continue - + if e.is_binary: continue - + # Read the README content, cut at 1MB readme = tree[e.name].data[:1048576].decode('UTF-8') break - + repo_size = sum(f.stat().st_size for f in pathlib.Path(path).glob("**/*")) - + try: with open(os.path.join(path, 'description')) as f: description = f.read() except: description = '' - + return template('repository/overview.html', readme=readme, repository=repository, @@ -293,60 +293,60 @@ def refs(repository): """ List repository refs """ - + repository += '.git' path = os.path.join(GITOLITE_REPOSITORIES_ROOT, repository) - + if not os.path.isdir(path): bottle.abort(404, 'No repository at this path.') - + repo = pygit2.Repository(path) - + if repo.is_empty: return template('repository/refs.html', repository=repository) - + try: HEAD = repo.head.name except: HEAD = None - + heads = [] tags = [] - + for ref in repo.references: ref = repo.references.get(ref) - + if not ref: continue - + if ref.name.startswith('refs/heads/'): heads.append({ 'ref': ref, 'commit': ref.peel(pygit2.GIT_OBJ_COMMIT) }) - + if ref.name.startswith('refs/tags/'): target = repo.get(str(ref.target)) - + tags.append({ 'ref': ref, 'object': target, 'is_annotated': target.type == pygit2.GIT_OBJ_TAG }) - + heads.sort(key = lambda item: item['ref'].name) - + def tagsort(item): try: if item['object'].type == pygit2.GIT_OBJ_TAG: return item['object'].tagger.time - + if item['object'].type == pygit2.GIT_OBJ_COMMIT: return item['object'].commit_time except: return 0 - + tags.sort(key = lambda item: tagsort(item), reverse=True) - + return template('repository/refs.html', repository=repository, heads=heads, tags=tags, HEAD=HEAD) @@ -357,24 +357,24 @@ def tree(repository, revision, tree_path=None): """ Show commit tree. """ - + repository += '.git' repository_path = os.path.join(GITOLITE_REPOSITORIES_ROOT, repository) - + if not os.path.isdir(repository_path): bottle.abort(404, 'No repository at this path.') - + repo = pygit2.Repository(repository_path) - + if repo.is_empty: return template('repository/tree.html', repository=repository, revision=revision, offset=0) - + try: git_object = repo.revparse_single(revision) except: bottle.abort(404) - + # List all the references. # This is used for allowing the user to switch revision with a selector. HEAD = None @@ -385,21 +385,21 @@ def tree(repository, revision, tree_path=None): if ref.startswith('refs/tags/'): tags.append(ref) heads.sort() tags.sort() - + try: HEAD = repo.head.name except: pass - + if git_object.type == pygit2.GIT_OBJ_TAG: git_object = git_object.peel(None) - + if git_object.type == pygit2.GIT_OBJ_COMMIT: git_object = git_object.tree - + if git_object.type == pygit2.GIT_OBJ_TREE and tree_path: git_object = git_object[tree_path] - + if git_object.type == pygit2.GIT_OBJ_TREE: return template( 'repository/tree.html', @@ -407,26 +407,26 @@ def tree(repository, revision, tree_path=None): tree=git_object, tree_path=tree_path, repository=repository, revision=revision) - + if git_object.type == pygit2.GIT_OBJ_BLOB: - + # Highlight blob text if git_object.is_binary: blob_formatted = '' else: blob_data = git_object.data.decode('UTF-8') - + # Guess Pygments lexer by filename, or by content if we can't find one try: pygments_lexer = guess_lexer_for_filename(git_object.name, blob_data) except: pygments_lexer = guess_lexer(blob_data) - + pygments_formatter = HtmlFormatter(nobackground=True, linenos=True, anchorlinenos=True, lineanchors='line') - + blob_formatted = highlight(blob_data, pygments_lexer, pygments_formatter) - + return template( 'repository/blob.html', heads=heads, tags=tags, @@ -434,7 +434,7 @@ def tree(repository, revision, tree_path=None): blob_formatted=blob_formatted, repository=repository, revision=revision, tree_path=tree_path) - + bottle.abort(404) @bottle.post('/.git/tree', name='tree_change') @@ -444,9 +444,9 @@ def tree_change(repository): This route is used by the
in the tree page when changing the revision to be displayed. """ - + revision = request.forms.get('revision') - + bottle.redirect(application.get_url('tree', repository=repository, revision=revision)) @@ -456,28 +456,28 @@ def log(repository, revision): """ Show commit log. """ - + repository += '.git' repository_path = os.path.join(GITOLITE_REPOSITORIES_ROOT, repository) - + # Read commits try: commits_offset = int(request.query.get('offset', 0)) except: commits_offset = 0 - + if not os.path.isdir(repository_path): bottle.abort(404, 'No repository at this path.') - + repo = pygit2.Repository(repository_path) - + if repo.is_empty: return template('repository/log.html', repository=repository, revision=revision, offset=commits_offset) - + try: git_object = repo.revparse_single(revision) except: bottle.abort(404) - + # List all the references. # This is used for allowing the user to switch revision with a selector. HEAD = None @@ -488,20 +488,20 @@ def log(repository, revision): if ref.startswith('refs/tags/'): tags.append(ref) heads.sort() tags.sort() - + try: HEAD = repo.head.name except: pass - + if git_object.type in [ pygit2.GIT_OBJ_TREE, pygit2.GIT_OBJ_BLOB ]: return 'Not a valid ref' - + if git_object.type == pygit2.GIT_OBJ_TAG: git_object = git_object.peel(None) - + # At this point git_object should be a valid pygit2.GIT_OBJ_COMMIT - + commits = [] diff = {} commit_ith = 0 @@ -510,20 +510,20 @@ def log(repository, revision): if commit_ith < commits_offset: commit_ith += 1 continue - + # Stop if we have reached pagination size if len(commits) >= LOG_PAGINATION: break - + commits.append(commit) - + # Diff with parent tree, or empty tree if there's no parent if LOG_STATS: diff[commit.short_id] = \ commit.parents[0].tree.diff_to_tree(commit.tree) \ if len(commit.parents) > 0 \ else commit.tree.diff_to_tree(swap=True) - + return template( 'repository/log.html', heads=heads, head_ref=HEAD, tags=tags, @@ -538,9 +538,9 @@ def log_change(repository): This route is used by the in the log page when changing the revision to be displayed. """ - + revision = request.forms.get('revision') - + bottle.redirect(application.get_url('log', repository=repository, revision=revision)) @@ -554,19 +554,19 @@ def commit(repository, commit_id, commit_id2=None): """ Show a commit. """ - + repository += '.git' repository_path = os.path.join(GITOLITE_REPOSITORIES_ROOT, repository) - + if not os.path.isdir(repository_path): bottle.abort(404, 'No repository at this path.') - + repo = pygit2.Repository(repository_path) - + try: commit = repo.get(commit_id) assert commit.type == pygit2.GIT_OBJ_COMMIT - + if commit_id2: commit2 = repo.get(commit_id2) assert commit2.type == pygit2.GIT_OBJ_COMMIT @@ -577,22 +577,22 @@ def commit(repository, commit_id, commit_id2=None): commit2 = None except: bottle.abort(404, 'Not a valid commit.') - + # Diff options - + diff_mode = DIFF_VIEW if 'mode' in request.query: if request.query.get('mode') in [ 'udiff', 'udiff_raw', 'ssdiff' ]: diff_mode = request.query.get('mode') else: bottle.abort(400, 'Bad request: mode') - + try: diff_context_lines = int(request.query.get('context_lines', DIFF_CONTEXT_LINES)) except: bottle.abort(400, 'Bad request: context_lines') - + try: diff_inter_hunk_lines = int(request.query.get('inter_hunk_lines', DIFF_INTERHUNK_LINES)) except: bottle.abort(400, 'Bad request: inter_hunk_lines') - + diff_flags = pygit2.GIT_DIFF_NORMAL diff_side = DIFF_SIDE if 'side' in request.query: @@ -603,7 +603,7 @@ def commit(repository, commit_id, commit_id2=None): diff_side = 'reverse' else: bottle.abort(400, 'Bad request: side') - + diff_whitespace = DIFF_WHITESPACE if 'whitespace' in request.query: if request.query.get('whitespace') == 'include': @@ -619,9 +619,9 @@ def commit(repository, commit_id, commit_id2=None): diff_whitespace = 'ignore_eol' else: bottle.abort(400, 'Bad request: whitespace') - + # Compute diff with parent - + if commit2: diff = repo.diff( a=commit2, b=commit, flags=diff_flags, @@ -633,21 +633,21 @@ def commit(repository, commit_id, commit_id2=None): interhunk_lines = diff_inter_hunk_lines, flags = diff_flags, swap = True) - + # Compute the similarity index. This is used to decide which files are "renamed". diff.find_similar() - + if request.route.name in [ 'commit_diff', 'commit_diff2' ]: response.content_type = 'text/plain' return diff.patch - + if request.route.name == 'commit_patch': """ Looks like pygit2 doesn't have a way for creating a patch file from a commit. So, exclusively for this task, we fork a new subprocess and read the output. TODO see if this subprocess call can be replaced with pygit2. """ - + try: output = subprocess.check_output( args=[ 'git', 'format-patch', @@ -657,13 +657,13 @@ def commit(repository, commit_id, commit_id2=None): '--inter-hunk-context={}'.format(diff_inter_hunk_lines), '-1', str(commit.id) ], cwd=repository_path) - + response.content_type = 'text/plain' return output except Exception as e: print(e) bottle.abort(500, 'Cannot create patch.') - + return template( 'repository/commit.html', repository=repository, commit=commit, commit2=commit2, diff=diff, @@ -675,38 +675,38 @@ def raw(repository, revision, tree_path): """ Return a raw blow object. """ - + repository += '.git' repository_path = os.path.join(GITOLITE_REPOSITORIES_ROOT, repository) - + if not os.path.isdir(repository_path): bottle.abort(404, 'No repository at this path.') - + repo = pygit2.Repository(repository_path) - + if repo.is_empty: return "" - + git_tree = None - + try: git_object = repo.revparse_single(revision) except: pass - + if not git_object or git_object.type != pygit2.GIT_OBJ_COMMIT: bottle.abort(404, 'Not a valid revision.') - + blob = None - + try: blob = git_object.tree[tree_path] except: bottle.abort(404, 'Object does not exist.') - + if blob.type != pygit2.GIT_OBJ_BLOB: bottle.abort(404, 'Object is not a blob.') - + mime = magic.from_buffer(blob.data[:1048576], mime=True) response.content_type = mime return blob.data @@ -721,25 +721,25 @@ def git_smart_http(repository): Note that this controller only matches "git-upload-pack" (used for fetching) but does not match "git-receive-pack" (used for pushing). Pushing should only happen via SSH. - + Note: If CLIF is running behind a web server such as httpd or lighttpd, the same behavior of this controller can be achieved much more simply by configuring the server with CGI and an alias that redirects the URLs above to the gitolite-shell script. However, this controller exists so that anonymous HTTP clones can work "out of the box" without any manual configuration of the server. - + Documentation useful for understanding how this works: https://git-scm.com/docs/http-protocol https://bottlepy.org/docs/dev/async.html https://gitolite.com/gitolite/http.html#allowing-unauthenticated-access """ - + # Environment variables for the Gitolite shell # TODO Gitolite gives a warning: "WARNING: Use of uninitialized value in concatenation (.) or string at /home/git/bin/gitolite-shell line 239" # Looks like some non-critical env vars are missing here: REMOTE_PORT SERVER_ADDR SERVER_PORT gitenv = { **os.environ, - + # https://git-scm.com/docs/git-http-backend#_environment 'PATH_INFO': request.path, 'REMOTE_USER': 'anonymous', # This user must be set in ~/.gitolite.rc like this: @@ -750,13 +750,13 @@ def git_smart_http(repository): 'REQUEST_METHOD': request.method, 'GIT_PROJECT_ROOT': GITOLITE_REPOSITORIES_ROOT, 'GIT_HTTP_EXPORT_ALL': 'true', - + # Additional variables required by Gitolite 'REQUEST_URI': request.fullpath, 'GITOLITE_HTTP_HOME': GITOLITE_HTTP_HOME, 'HOME': GITOLITE_HTTP_HOME, } - + # Start a Gitolite shell. # Do not replace .Popen() with .run() because it waits for child process to finish before returning. proc = subprocess.Popen( @@ -765,14 +765,14 @@ def git_smart_http(repository): stdin = subprocess.PIPE, stdout = subprocess.PIPE) # stderr = ) - + # Write the whole request body to Gitolite stdin. # Don't forget to close the pipe or it will hang! proc.stdin.write(request.body.read()) proc.stdin.close() - + # Now we process the Gitolite response and return it to the client. - + # First we need to scan all the HTTP headers in the response so that we can # add them to the bottle response... for line in proc.stdout: @@ -793,63 +793,63 @@ def git_smart_http(repository): def threads(repository): """ List email threads. - + :param repository: Match repository name NOT ending with ".git" """ - + # List of seletected tags, retrieved from the query string query_tags = { k: request.query.getall(k) for k in request.query.keys() } - + repository += '.mlist.git' path = os.path.join(GITOLITE_REPOSITORIES_ROOT, repository) list_address = '{}@{}'.format(repository[:-10], INSTANCE_DOMAIN) - + if not os.path.isdir(path): bottle.abort(404, 'No repository at this path.') - + try: repo = pygit2.Repository(path) tree = repo.revparse_single('HEAD').tree except: return template('mailing_list/emails.html', list_address=list_address, repository=repository) - + threads_list = [] tags = {} - + for obj in tree: if obj.type != pygit2.GIT_OBJ_TREE: continue - + thread_date, thread_time, thread_id, thread_title = obj.name.split(' ', 3) - + try: thread_tags = parse_thread_tags(obj['tags'].data.decode('UTF-8')) - + # Collect tags for filters for k, v in thread_tags.items(): tags[k] = tags.get(k, set()).union(v) except: thread_tags = {} - + # Check if we should filter out this thread from the list keep = True for key in query_tags.keys(): for value in query_tags[key]: action, value = value[0], value[1:] - + if action not in [ '+', '-' ]: bottle.abort(400, 'Bad request: {}'.format(key)) - + if action == '+' and value not in thread_tags.get(key, []): keep = False break - + if action == '-' and value in thread_tags.get(key, []): keep = False break - + if not keep: break - + if keep: threads_list.append({ 'datetime': thread_date + ' ' + thread_time, @@ -857,9 +857,9 @@ def threads(repository): 'title': thread_title, 'tags': thread_tags }) - + threads_list.reverse() - + return template('mailing_list/emails.html', threads=threads_list, list_address=list_address, repository=repository, @@ -870,55 +870,55 @@ def thread(repository, thread_id): """ Show a single email thread. """ - + repository += '.mlist.git' path = os.path.join(GITOLITE_REPOSITORIES_ROOT, repository) list_address = '{}@{}'.format(repository[:-10], INSTANCE_DOMAIN) - + if not os.path.isdir(path): bottle.abort(404, 'No repository at this path.') - + repo = pygit2.Repository(path) head_tree = repo.revparse_single('HEAD').tree thread_tree = None - + for obj in head_tree: if obj.type != pygit2.GIT_OBJ_TREE: continue - + if thread_id in obj.name: thread_tree = obj break - + if not thread_tree: bottle.abort(404, 'Not a valid thread') - + thread_date, thread_time, thread_id, thread_title = thread_tree.name.split(' ', 3) thread_data = { 'datetime': thread_date + ' ' + thread_time, 'id': thread_id, 'title': thread_title } - + # Read all the emails in this thread and collect some statistics on the way (for # displaying purposes only) emails = [] participants = [] tags = {} - + for obj in thread_tree: if obj.type != pygit2.GIT_OBJ_BLOB: continue - + if obj.name == 'tags': tags = parse_thread_tags(obj.data.decode('UTF-8')) continue - + if not obj.name.endswith('.email'): continue - + message = email.message_from_string(obj.data.decode('UTF-8'), policy=email.policy.default) - + email_data = { 'id': message.get('message-id'), 'id_hash': hashlib.sha256(message.get('message-id').encode('utf-8')).hexdigest()[:8], @@ -930,14 +930,14 @@ def thread(repository, thread_id): 'subject': message.get('subject'), 'body': message.get_body(('plain',)).get_content() } - + emails.append(email_data) - + if email_data['from'] not in participants: participants.append(email_data['from']) - + emails.sort(key = lambda email: email['received_at']) - + return template('mailing_list/emails_thread.html', thread=thread_data, emails=emails, participants=participants, list_address=list_address, tags=tags, repository=repository)