Files
developtools_integration_ve…/tools/code_access_control/sdk_check_comment.py
T
liangliang 8e556d29a4 sdk代码修改注释信息解析脚本
Signed-off-by: liangliang <xuliang110@huawei.com>
2026-01-27 20:00:06 +08:00

510 lines
19 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright (c) 2026 Huawei Device Co., Ltd.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import os
import sys
import subprocess
import re
from typing import List, Tuple, Dict, Set
import argparse
from pathlib import Path
import tempfile
import shutil
class GitDiffAnalyzer:
"""Git diff analyzer to check if modifications are comments only"""
def __init__(self, repo_path: str = None, commit_ref: str = "HEAD"):
"""
Initialize the analyzer
Args:
repo_path: Git repository path, None means current directory
commit_ref: Commit reference to check, defaults to HEAD
"""
self.repo_path = repo_path or os.getcwd()
self.commit_ref = commit_ref
self.diff_content = ""
self.modified_files: List[str] = []
self.comment_patterns = {
'c': [r'^\s*//', r'^\s*/\*', r'\*/', r'^\s*///', r'^\s*//!'],
'cpp': [r'^\s*//', r'^\s*/\*', r'\*/', r'^\s*///', r'^\s*//!'],
'h': [r'^\s*//', r'^\s*/\*', r'\*/', r'^\s*///', r'^\s*//!'],
'hpp': [r'^\s*//', r'^\s*/\*', r'\*/', r'^\s*///', r'^\s*//!'],
'java': [r'^\s*//', r'^\s*/\*', r'\*/', r'^\s*/\*\*', r'\*/'],
'js': [r'^\s*//', r'^\s*/\*', r'\*/', r'^\s*///', r'^\s*//!'],
'ts': [r'^\s*//', r'^\s*/\*', r'\*/', r'^\s*///', r'^\s*//!'],
'go': [r'^\s*//', r'^\s*/\*', r'\*/'],
'rs': [r'^\s*//', r'^\s*/\*', r'\*/', r'^\s*///', r'^\s*//!'],
'swift': [r'^\s*//', r'^\s*/\*', r'\*/'],
'cs': [r'^\s*//', r'^\s*/\*', r'\*/', r'^\s*///', r'^\s*//!'],
'py': [r'^\s*#', r'^\s*"""', r'^\s*\'\'\''],
'sh': [r'^\s*#'],
'bash': [r'^\s*#'],
'zsh': [r'^\s*#'],
'html': [r'^\s*<!--', r'-->'],
'htm': [r'^\s*<!--', r'-->'],
'xml': [r'^\s*<!--', r'-->'],
'vue': [r'^\s*<!--', r'-->', r'^\s*//', r'^\s*/\*'],
'css': [r'^\s*/\*', r'\*/'],
'scss': [r'^\s*//', r'^\s*/\*', r'\*/'],
'less': [r'^\s*//', r'^\s*/\*', r'\*/'],
'rb': [r'^\s*#', r'^\s*=begin', r'^\s*=end'],
'php': [r'^\s*//', r'^\s*#', r'^\s*/\*', r'\*/'],
'sql': [r'^\s*--', r'^\s*/\*', r'\*/'],
'md': [r'^\s*<!--', r'-->'],
'yaml': [r'^\s*#'],
'yml': [r'^\s*#'],
'json': [],
'ini': [r'^\s*;', r'^\s*#'],
'toml': [r'^\s*#'],
'cfg': [r'^\s*#'],
'conf': [r'^\s*#'],
'mk': [r'^\s*#'],
'makefile': [r'^\s*#'],
'dockerfile': [r'^\s*#'],
'txt': [],
}
self.extension_map = {}
for ext, patterns in self.comment_patterns.items():
self.extension_map[ext] = patterns
def run_git_command(self, cmd: List[str]) -> Tuple[bool, str]:
"""
Execute Git command
Args:
cmd: Git command list
Returns:
(success flag, output content)
"""
try:
result = subprocess.run(
['git'] + cmd,
cwd=self.repo_path,
capture_output=True,
text=True,
check=True
)
return True, result.stdout
except subprocess.CalledProcessError as e:
return False, f"Git error: {e.stderr}"
except FileNotFoundError:
return False, "Git not install"
def generate_diff_file(self, output_path: str = None) -> Tuple[bool, str]:
"""
Generate diff file
Args:
output_path: Diff file output path, None means use temporary file
Returns:
(success flag, diff file path or error message)
"""
success, output = self.run_git_command(["log", "--oneline", "-2", "--format=%H"])
if not success:
return False, output
commits = output.strip().split('\n')
if len(commits) < 2:
return False, "need two pf"
current_commit = commits[0].strip()
prev_commit = commits[1].strip()
success, diff_output = self.run_git_command([
"diff",
f"{prev_commit}..{current_commit}",
"--no-ext-diff"
])
if not success:
return False, diff_output
self.diff_content = diff_output
if output_path:
diff_path = output_path
try:
with open(diff_path, 'w', encoding='utf-8') as f:
f.write(diff_output)
except IOError as e:
return False, f"write: {e}"
else:
with tempfile.NamedTemporaryFile(mode='w', suffix='.diff', delete=False, encoding='utf-8') as f:
f.write(diff_output)
diff_path = f.name
self._extract_modified_files(diff_output)
return True, diff_path
def _extract_modified_files(self, diff_content: str):
"""Extract modified files from diff content"""
files = []
lines = diff_content.split('\n')
for line in lines:
if line.startswith('diff --git'):
parts = line.split()
if len(parts) >= 3:
file_path = parts[2][2:]
files.append(file_path)
self.modified_files = files
def parse_diff_content(self, diff_content: str = None) -> List[Dict]:
"""
Parse diff content
Args:
diff_content: Diff content, None means use self.diff_content
Returns:
List of parsed diff hunks
"""
if diff_content is None:
diff_content = self.diff_content
if not diff_content:
return []
lines = diff_content.split('\n')
hunks = []
current_hunk = None
current_file = None
i = 0
while i < len(lines):
line = lines[i]
if line.startswith('diff --git'):
if current_hunk and current_file:
hunks.append(current_hunk)
parts = line.split()
file_path = parts[2][2:]
current_file = file_path
current_hunk = {
'file': file_path,
'old_start': 0,
'old_lines': 0,
'new_start': 0,
'new_lines': 0,
'changes': []
}
i += 1
while i < len(lines) and not lines[i].startswith('@@'):
i += 1
elif line.startswith('@@'):
if current_hunk:
match = re.match(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@', line)
if match:
current_hunk['old_start'] = int(match.group(1))
current_hunk['old_lines'] = int(match.group(2) or 1)
current_hunk['new_start'] = int(match.group(3))
current_hunk['new_lines'] = int(match.group(4) or 1)
i += 1
elif current_hunk and (line.startswith('+') or line.startswith('-') or line.startswith(' ')):
change_type = line[0]
content = line[1:] if line else ''
current_hunk['changes'].append((change_type, content))
i += 1
else:
i += 1
if current_hunk and current_file:
hunks.append(current_hunk)
return hunks
def get_file_extension(self, file_path: str) -> str:
"""
Get file extension (without dot)
Args:
file_path: File path
Returns:
Extension (lowercase)
"""
filename = os.path.basename(file_path).lower()
if filename == 'dockerfile':
return 'dockerfile'
elif filename == 'makefile':
return 'makefile'
ext = os.path.splitext(file_path)[1]
if ext:
return ext[1:].lower()
return ''
def is_comment_line(self, line: str, file_extension: str) -> bool:
"""
Determine if a line is a comment
Args:
line: Code line
file_extension: File extension
Returns:
Whether it's a comment
"""
stripped_line = line.strip()
if not stripped_line:
return True
patterns = self.comment_patterns.get(file_extension, [])
for pattern in patterns:
if re.search(pattern, line):
return True
if file_extension in ['c', 'cpp', 'java', 'js', 'ts', 'go', 'rs', 'cs']:
if '/*' in line and '*/' in line:
comment_start = line.find('/*')
comment_end = line.find('*/') + 2
code_before = line[:comment_start].strip()
code_after = line[comment_end:].strip()
if not code_before and not code_after:
return True
if stripped_line.startswith('*/'):
return True
if file_extension == 'py':
if stripped_line.startswith('"""') or stripped_line.startswith("'''"):
return True
if file_extension in ['html', 'htm', 'xml', 'vue']:
if '<!--' in line and '-->' in line:
return True
return False
def analyze_hunk(self, hunk: Dict) -> Tuple[bool, List[str]]:
"""
Analyze whether a diff hunk only contains comment modifications
Args:
hunk: Diff hunk dictionary
Returns:
(whether only comments modified, non-comment change list)
"""
file_path = hunk['file']
file_ext = self.get_file_extension(file_path)
non_comment_changes = []
for change_type, content in hunk['changes']:
if change_type in ('+', '-'):
if not self.is_comment_line(content, file_ext):
non_comment_changes.append(content)
return len(non_comment_changes) == 0, non_comment_changes
def check_commit_is_comment_only(self, diff_path: str = None) -> Tuple[bool, Dict]:
"""
Check if commit only modifies comments
Args:
diff_path: Diff file path, None means generate automatically
Returns:
(whether only comments modified, detailed analysis result)
"""
if diff_path:
try:
with open(diff_path, 'r', encoding='utf-8') as f:
diff_content = f.read()
self.diff_content = diff_content
self._extract_modified_files(diff_content)
except IOError as e:
return False, {'error': f'no read diff file: {e}'}
else:
success, result = self.generate_diff_file()
if not success:
return False, {'error': result}
diff_path = result
hunks = self.parse_diff_content()
if not hunks:
return True, {
'message': 'not change',
'modified_files': self.modified_files,
'hunks': []
}
all_comment_only = True
analysis_results = []
all_non_comment_changes = []
for hunk in hunks:
is_comment_only, non_comment_changes = self.analyze_hunk(hunk)
analysis_results.append({
'file': hunk['file'],
'is_comment_only': is_comment_only,
'non_comment_changes': non_comment_changes,
'hunk_info': {
'old_start': hunk['old_start'],
'new_start': hunk['new_start']
}
})
if not is_comment_only:
all_comment_only = False
all_non_comment_changes.extend(non_comment_changes)
result = {
'is_comment_only': all_comment_only,
'modified_files': self.modified_files,
'analysis_results': analysis_results,
'non_comment_changes': all_non_comment_changes,
'diff_file': diff_path
}
return all_comment_only, result
def print_analysis_report(result: Dict, verbose: bool = False):
print("\n" + "="*60)
print("Git")
print("="*60)
if 'error' in result:
print(f"error: {result['error']}")
return
if result['modified_files']:
for file in result['modified_files']:
print(f" - {file}")
if verbose and result['analysis_results']:
for analysis in result['analysis_results']:
status = "true" if analysis['is_comment_only'] else "false"
print(f"\n: {analysis['file']} ({status})")
if not analysis['is_comment_only'] and analysis['non_comment_changes']:
for change in analysis['non_comment_changes'][:5]:
print(f" - {change[:80]}{'...' if len(change) > 80 else ''}")
if not result['is_comment_only'] and result['non_comment_changes']:
print(f"\n {len(result['non_comment_changes'])} ")
if 'diff_file' in result and os.path.exists(result['diff_file']):
print(f"\nDiff: {result['diff_file']}")
print("\n" + "="*60)
@dataclass
class DiffChange:
file_path: str
start_line_old: int
lines_old: int
start_line_new: int
lines_new: int
deleted_lines: List[str]
added_lines: List[str]
context_lines: List[str]
class DiffParser:
def __init__(self, diff_file_path: str):
self.diff_file_path = diff_file_path
self.diff_git_pattern = re.compile(r'diff --git a/(.*) b/(.*)')
self.hunk_header_pattern = re.compile(r'@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
def parse(self) -> List[DiffChange]:
changes = []
current_change: Optional[DiffChange] = None
with open(self.diff_file_path, 'r', encoding='utf-8', errors='ignore') as f:
for line in f:
line = line.strip('\n')
diff_git_match = self.diff_git_pattern.match(line)
if diff_git_match:
file_path = diff_git_match.group(1)
current_change = None
continue
hunk_match = self.hunk_header_pattern.match(line)
if hunk_match and file_path:
start_old = int(hunk_match.group(1))
lines_old = int(hunk_match.group(3)) if hunk_match.group(3) else 1
start_new = int(hunk_match.group(4))
lines_new = int(hunk_match.group(6)) if hunk_match.group(6) else 1
current_change = DiffChange(
file_path=file_path,
start_line_old=start_old,
lines_old=lines_old,
start_line_new=start_new,
lines_new=lines_new,
deleted_lines=[],
added_lines=[],
context_lines=[]
)
changes.append(current_change)
continue
if current_change:
if line.startswith('-'):
current_change.deleted_lines.append(line[1:].lstrip())
elif line.startswith('+'):
current_change.added_lines.append(line[1:].lstrip())
elif line.startswith('\\'):
continue
else:
current_change.context_lines.append(line.lstrip())
return changes
def print_changes(self, changes: List[DiffChange]):
if not changes:
return
for idx, change in enumerate(changes, 1):
print(f"\n=== {idx} ===")
print(f"{change.file_path}")
print(f"{change.start_line_old} , {change.lines_old} ")
print(f" {change.start_new} , {change.lines_new} ")
if change.deleted_lines:
print("\n del:")
for line in change.deleted_lines:
print(f" - {line}")
if change.added_lines:
print("\n add:")
for line in change.added_lines:
print(f" + {line}")
if change.context_lines:
print("\n change:")
for line in change.context_lines:
print(f" {line}")
def main():
parser = argparse.ArgumentParser(
description='comment',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
%(prog)s
%(prog)s --diff my.diff
%(prog)s --repo /path/to/repo
%(prog)s --commit HEAD~2
%(prog)s --verbose
%(prog)s --output result.json
"""
)
parser.add_argument('--repo', '-r')
parser.add_argument('--commit', '-c', default='HEAD')
parser.add_argument('--diff', '-d')
parser.add_argument('--verbose', '-v', action='store_true')
parser.add_argument('--output', '-o')
parser.add_argument('--keep-diff', action='store_true')
parser.add_argument('--no-color', action='store_true')
args = parser.parse_args()
analyzer = GitDiffAnalyzer(args.repo, args.commit)
if not args.diff:
success, _ = analyzer.run_git_command(["rev-parse", "--git-dir"])
if not success:
sys.exit(1)
is_comment_only, result = analyzer.check_commit_is_comment_only(args.diff)
print_analysis_report(result, args.verbose)
if args.output:
import json
try:
with open(args.output, 'w', encoding='utf-8') as f:
json.dump(result, f, indent=2, ensure_ascii=False)
print(f"\n: {args.output}")
except IOError as e:
print(f"\nerror: {e}")
if not args.keep_diff and 'diff_file' in result:
diff_path = result['diff_file']
if os.path.exists(diff_path) and not args.diff:
try:
os.unlink(diff_path)
except:
pass
sys.exit(0 if is_comment_only else 1)
if __name__ == "__main__":
main()