feat: Add patchset history and diff support with cvsps integration
- Add cvsps package to Docker dependencies - Implement patchset retrieval and diff endpoints in API - Add _run_cvsps_command() helper for cvsps integration - Enhance file history parsing with log message extraction - Improve UI with enhanced styling and patchset functionality
This commit is contained in:
parent
c263092c10
commit
d3b40ae93f
9 changed files with 660 additions and 26 deletions
|
|
@ -84,6 +84,52 @@ class CVSClient:
|
|||
# Re-raise to allow caller to handle specific errors
|
||||
raise
|
||||
|
||||
def _run_cvsps_command(self, command, cwd=None):
|
||||
"""
|
||||
Run a cvsps command (standalone program, not a CVS subcommand)
|
||||
|
||||
:param command: List of cvsps command arguments
|
||||
:type command: list
|
||||
:param cwd: Working directory for the command
|
||||
:type cwd: str, optional
|
||||
:return: Command output as string
|
||||
:rtype: str
|
||||
:raises subprocess.CalledProcessError: If the cvsps command fails
|
||||
"""
|
||||
full_command = ['cvsps'] + command
|
||||
|
||||
# Debug printout of the command to be executed
|
||||
print(f"DEBUG: Executing cvsps command: {' '.join(full_command)}", file=sys.stderr)
|
||||
print(f"DEBUG: Working directory: {cwd or self.local_repo_path}", file=sys.stderr)
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
full_command,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
cwd=cwd or self.local_repo_path,
|
||||
env={**os.environ, 'CVSROOT': self.repo_url}
|
||||
)
|
||||
|
||||
# Debug printout of stdout and stderr
|
||||
if result.stdout:
|
||||
print(f"DEBUG: cvsps Command STDOUT:\n{result.stdout}", file=sys.stderr)
|
||||
if result.stderr:
|
||||
print(f"DEBUG: cvsps Command STDERR:\n{result.stderr}", file=sys.stderr)
|
||||
|
||||
return result.stdout.strip()
|
||||
except subprocess.CalledProcessError as e:
|
||||
# Debug printout for command failure
|
||||
print(f"DEBUG: cvsps Command FAILED", file=sys.stderr)
|
||||
print(f"DEBUG: Return Code: {e.returncode}", file=sys.stderr)
|
||||
print(f"DEBUG: Command: {' '.join(e.cmd)}", file=sys.stderr)
|
||||
print(f"DEBUG: STDOUT:\n{e.stdout}", file=sys.stderr)
|
||||
print(f"DEBUG: STDERR:\n{e.stderr}", file=sys.stderr)
|
||||
|
||||
# Re-raise to allow caller to handle specific errors
|
||||
raise
|
||||
|
||||
def _checkout_repository(self):
|
||||
"""
|
||||
Checkout or update the local repository
|
||||
|
|
@ -179,12 +225,13 @@ class CVSClient:
|
|||
:rtype: list
|
||||
"""
|
||||
try:
|
||||
# Use 'cvs log' instead of 'rlog' as it's more widely supported
|
||||
# Use 'cvs log' to get revision history for the file
|
||||
output = self._run_cvs_command(['log', file_path])
|
||||
|
||||
# Parse log output to extract revision details
|
||||
revisions = []
|
||||
current_revision = {}
|
||||
in_log = False
|
||||
|
||||
for line in output.split('\n'):
|
||||
# Look for revision lines (format: "revision X.X")
|
||||
|
|
@ -198,6 +245,7 @@ class CVSClient:
|
|||
if current_revision and 'revision' in current_revision:
|
||||
revisions.append(current_revision)
|
||||
current_revision = {'revision': rev_match.group(1)}
|
||||
in_log = False
|
||||
|
||||
if date_match:
|
||||
current_revision.update({
|
||||
|
|
@ -206,11 +254,35 @@ class CVSClient:
|
|||
'state': date_match.group(3),
|
||||
'lines_changed': 'N/A' # cvs log doesn't provide line counts
|
||||
})
|
||||
in_log = False
|
||||
|
||||
# Capture log message (lines after date/author/state until next revision or separator)
|
||||
if in_log:
|
||||
if line.strip() == '' or re.match(r'^---', line):
|
||||
in_log = False
|
||||
elif not re.match(r'^(revision|date|branches):', line):
|
||||
if 'log' not in current_revision:
|
||||
current_revision['log'] = ''
|
||||
if current_revision['log']:
|
||||
current_revision['log'] += '\n' + line
|
||||
else:
|
||||
current_revision['log'] = line
|
||||
|
||||
# Start capturing log after date line
|
||||
if date_match:
|
||||
in_log = True
|
||||
|
||||
# Add the last revision
|
||||
if current_revision and 'revision' in current_revision:
|
||||
revisions.append(current_revision)
|
||||
|
||||
# Clean up logs - strip whitespace and take first line only
|
||||
for rev in revisions:
|
||||
if 'log' in rev:
|
||||
rev['log'] = rev['log'].strip().split('\n')[0]
|
||||
else:
|
||||
rev['log'] = ''
|
||||
|
||||
return revisions
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"DEBUG: Error getting file history: {e}", file=sys.stderr)
|
||||
|
|
@ -235,4 +307,125 @@ class CVSClient:
|
|||
|
||||
return self._run_cvs_command(command)
|
||||
except subprocess.CalledProcessError:
|
||||
return f"Error retrieving content for {file_path}"
|
||||
return f"Error retrieving content for {file_path}"
|
||||
|
||||
def get_patchsets(self):
|
||||
"""
|
||||
Get repository patchset history using cvsps command
|
||||
|
||||
Note: cvsps must be run from the module directory, not the repository root
|
||||
|
||||
:return: List of patchset details
|
||||
:rtype: list
|
||||
"""
|
||||
try:
|
||||
# Determine the working directory for cvsps
|
||||
# If a module is specified, use the module directory; otherwise use the repository root
|
||||
if self.cvs_module:
|
||||
module_path = os.path.join(self.local_repo_path, self.cvs_module)
|
||||
cvsps_cwd = module_path if os.path.exists(module_path) else self.local_repo_path
|
||||
else:
|
||||
cvsps_cwd = self.local_repo_path
|
||||
|
||||
# Use 'cvsps' to get all patch sets in the repository
|
||||
# cvsps must be run from the module directory
|
||||
output = self._run_cvsps_command([], cwd=cvsps_cwd)
|
||||
|
||||
# Parse cvsps output to extract patchset details
|
||||
patchsets = []
|
||||
current_patchset = {}
|
||||
in_log = False
|
||||
|
||||
for line in output.split('\n'):
|
||||
# Look for PatchSet lines (format: "PatchSet XXXX")
|
||||
ps_match = re.match(r'^PatchSet\s+(\d+)', line)
|
||||
|
||||
# Look for Date line (format: "Date: YYYY/MM/DD HH:MM:SS")
|
||||
date_match = re.match(r'^Date:\s+(\d{4}/\d{2}/\d{2}\s+\d{2}:\d{2}:\d{2})', line)
|
||||
|
||||
# Look for Author line (format: "Author: NAME")
|
||||
author_match = re.match(r'^Author:\s+(\S+)', line)
|
||||
|
||||
# Look for Tag line (format: "Tag: TAG_NAME")
|
||||
tag_match = re.match(r'^Tag:\s+(\S+)', line)
|
||||
|
||||
# Look for Log line (format: "Log:")
|
||||
log_match = re.match(r'^Log:', line)
|
||||
|
||||
if ps_match:
|
||||
# Start of a new patch set
|
||||
if current_patchset and 'patchset' in current_patchset:
|
||||
patchsets.append(current_patchset)
|
||||
current_patchset = {'patchset': ps_match.group(1)}
|
||||
in_log = False
|
||||
|
||||
if date_match:
|
||||
current_patchset['date'] = date_match.group(1)
|
||||
|
||||
if author_match:
|
||||
current_patchset['author'] = author_match.group(1)
|
||||
|
||||
if tag_match:
|
||||
current_patchset['tag'] = tag_match.group(1)
|
||||
|
||||
if log_match:
|
||||
# Log section starts, capture until next PatchSet or end
|
||||
current_patchset['log'] = ''
|
||||
in_log = True
|
||||
elif in_log:
|
||||
# Capture log lines until we hit an empty line or next section
|
||||
if line.strip() == '':
|
||||
in_log = False
|
||||
elif not re.match(r'^(PatchSet|Date|Author|Tag|Files):', line):
|
||||
# Append to log if it's not a new section header
|
||||
if current_patchset['log']:
|
||||
current_patchset['log'] += '\n' + line
|
||||
else:
|
||||
current_patchset['log'] = line
|
||||
|
||||
# Add the last patchset
|
||||
if current_patchset and 'patchset' in current_patchset:
|
||||
patchsets.append(current_patchset)
|
||||
|
||||
# Ensure all patchsets have the required fields with defaults
|
||||
for ps in patchsets:
|
||||
if 'date' not in ps:
|
||||
ps['date'] = 'N/A'
|
||||
if 'author' not in ps:
|
||||
ps['author'] = 'N/A'
|
||||
if 'tag' not in ps:
|
||||
ps['tag'] = 'N/A'
|
||||
if 'log' not in ps:
|
||||
ps['log'] = ''
|
||||
# Clean up log - remove leading/trailing whitespace and limit to first line for compact display
|
||||
ps['log'] = ps['log'].strip()
|
||||
|
||||
return patchsets
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"DEBUG: Error getting patchsets: {e}", file=sys.stderr)
|
||||
return []
|
||||
|
||||
def get_patchset_diff(self, patchset_number):
|
||||
"""
|
||||
Get diff for a specific patchset using cvsps command
|
||||
|
||||
:param patchset_number: Patchset number
|
||||
:type patchset_number: str
|
||||
:return: Diff output for the patchset
|
||||
:rtype: str
|
||||
"""
|
||||
try:
|
||||
# Determine the working directory for cvsps
|
||||
# If a module is specified, use the module directory; otherwise use the repository root
|
||||
if self.cvs_module:
|
||||
module_path = os.path.join(self.local_repo_path, self.cvs_module)
|
||||
cvsps_cwd = module_path if os.path.exists(module_path) else self.local_repo_path
|
||||
else:
|
||||
cvsps_cwd = self.local_repo_path
|
||||
|
||||
# Use 'cvsps' with -s flag to select the patchset and -g flag to generate the diff
|
||||
output = self._run_cvsps_command(['-s', patchset_number, '-g'], cwd=cvsps_cwd)
|
||||
return output
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"DEBUG: Error getting patchset diff: {e}", file=sys.stderr)
|
||||
return f"Error retrieving diff for patchset {patchset_number}"
|
||||
Loading…
Add table
Add a link
Reference in a new issue