Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

P4: Add support for json output #38

Open
wants to merge 17 commits into
base: dev
Choose a base branch
from
Open
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 73 additions & 4 deletions nimp/utils/p4.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@
''' Perforce utilities '''

import argparse
import json
import logging
import os
import os.path
import re
import tempfile

import nimp.sys.process
import nimp.system
Expand Down Expand Up @@ -228,6 +230,22 @@ def reconcile(self, cl_number, *files):

return ret

def reconcile_workspace(self, *paths_to_reconcile, cl_number=None, dry_run=False):
''' Reconciles given workspace '''

p4_reconcile_args = ['-f', '-e', '-a', '-d']
if dry_run:
p4_reconcile_args.append('-n')
if cl_number:
p4_reconcile_args.extend(['-c', cl_number])
for path_to_reconcile in paths_to_reconcile:
if not path_to_reconcile.endswith('...'):
if os.path.isdir(path_to_reconcile) or path_to_reconcile.endswith(('/', '\\')):
path_to_reconcile = os.path.join(path_to_reconcile, '...')
p4_reconcile_args.append(path_to_reconcile)

return self._run_using_arg_file('reconcile', *p4_reconcile_args) is not None

def get_changelist_description(self, cl_number):
''' Returns description of given changelist '''
desc, = next(self._parse_command_output(["describe", cl_number], r"\.\.\. desc (.*)"))
Expand All @@ -248,6 +266,21 @@ def get_last_synced_changelist(self):

return cl_number

def get_file_workspace_current_revision(self, file):
''' Returns the file revision currently synced in the workspace '''
return next(self._parse_command_output(['have', file], r'\.\.\. haveRev (\d+)'), default=None)

def print(self, p4_print_command_args):
jasugun marked this conversation as resolved.
Show resolved Hide resolved
''' wrapper for p4 print command '''
data = ''
output = self._run('print', p4_print_command_args, use_json_format=True, hide_output=True)
output = [json_element for json_element in output.splitlines() if json_element]
for output_chunk in output:
output_chunk = json.loads(output_chunk)
if 'data' in output_chunk:
data += output_chunk['data']
return data

def get_or_create_changelist(self, description):
''' Creates or returns changelist number if it's not already created '''
pending_changelists = self.get_pending_changelists()
Expand Down Expand Up @@ -335,6 +368,26 @@ def submit(self, cl_number):

return True

def submit_default_changelist(self, description=None, revert_unchanged=False, dry_run=False):
''' Submits given changelist '''
logging.info("Submitting default changelist...")
submit_args = []
if revert_unchanged:
submit_args.extend(['-f', 'revertunchanged'])
if description:
# description has to fit on one line, even when using -x arg_file
# there is a perforce limitation to how long the desc can be however, arg_file or not, I tested this.
# p4 command failed: Identifiers too long. Must not be longer than 1024 bytes of UTF-8
# ...happens when using 130000-ish bytes utf8 words
# anything under that seems to be fine, whatever happened to this 1024 bytes limit...
# couldn't find more info on this subject in the perforce documentation
description_limit = 120000
submit_args.extend(['-d', description[:description_limit]])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the -x arg expect a file with one arg per line.
Should we escape the line endings (\n, \r\n, etc) from the desc?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right.
Removing \n solves the issue. I haven't found way a to esacpe them though, while conserving line breaks.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're now using the utils.p4.submit(description=p4_desc):

  • It should avoid the escaping hell issue by just updating the default changelist spec before submitting.
  • I'll test some commit messages from our monorepo though to be sure nothing weird happens

if dry_run:
logging.info(f'{self._get_p4_command("submit")} {submit_args}')
return True
return self._run_using_arg_file('submit', *submit_args) is not None

def sync(self, *files, cl_number = None):
''' Udpate given file '''
command = ["sync"]
Expand Down Expand Up @@ -370,8 +423,10 @@ def _escape_filename(name):
.replace('#', '%23') \
.replace('*', '%2A')

def _get_p4_command(self, *args):
def _get_p4_command(self, *args, use_json_format=False):
command = ['p4', '-z', 'tag']
if use_json_format:
command.append('-Mj')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's recommended to always use -ztag with -Mj

if self._port is not None:
command += ['-p', self._port]
if self._user is not None:
Expand All @@ -384,11 +439,12 @@ def _get_p4_command(self, *args):
command += list(args)
return command

def _run(self, *args, stdin=None, hide_output=False):
command = self._get_p4_command(*args)
def _run(self, *args, stdin=None, hide_output=False, use_json_format=False):
command = self._get_p4_command(*args, use_json_format=use_json_format)

for _ in range(5):
result, output, error = nimp.sys.process.call(command, stdin=stdin, encoding='cp437', capture_output=True, hide_output=hide_output)
result, output, error = nimp.sys.process.call(
command, stdin=stdin, encoding='cp437', capture_output=True, hide_output=hide_output)

if 'Operation took too long ' in error:
continue
Expand All @@ -405,6 +461,19 @@ def _run(self, *args, stdin=None, hide_output=False):

return output

def _run_using_arg_file(self, command, *command_args):
''' runs p4 <args...> -x arg_file_containing_command_args command '''
# perforce seems unable to handle a tempfile.TemporaryFile():
# p4 command failed: Perforce client error: open for read:
# <temp_file_path>: The process cannot access the file because it is being used by another process
# Use a temp dir instead so the temp file can be used by perforce...
# The dir and file is wiped anyway when exiting the context manager
with tempfile.TemporaryDirectory(prefix="p4_arg_file_") as tmp_dir:
arg_file_path = os.path.normpath(os.path.join(tmp_dir, 'p4_arg_file'))
with open(arg_file_path, 'w') as fp:
fp.write('\n'.join(command_args))
return self._run(*['-x', arg_file_path, command])

def _parse_command_output(self, command, *patterns, stdin = None, hide_output = False):
jasugun marked this conversation as resolved.
Show resolved Hide resolved
output = self._run(*command, stdin = stdin, hide_output = hide_output)

Expand Down