1#!/usr/bin/python3 -B
2
3# Copyright 2021 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#      http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16"""ojluni_modify_expectation is a command-line tool for modifying the EXPECTED_UPSTREAM file."""
17
18import argparse
19import sys
20
21# pylint: disable=g-importing-member
22# pylint: disable=g-multiple-import
23from typing import (
24    Sequence,
25    List,
26)
27
28from common_util import (
29    ExpectedUpstreamEntry,
30    ExpectedUpstreamFile,
31    LIBCORE_DIR,
32    OpenjdkFinder,
33    OjluniFinder,
34)
35
36# Import git only after common_util because common_util will
37# produce informative error
38from git import (Commit, Repo)
39from gitdb.exc import BadName
40
41LIBCORE_REPO = Repo(LIBCORE_DIR.as_posix())
42
43AUTOCOMPLETE_TAGS = [
44    'jdk7u/jdk7u40-b60',
45    'jdk8u/jdk8u121-b13',
46    'jdk8u/jdk8u60-b31',
47    'jdk9/jdk-9+181',
48    'jdk11u/jdk-11.0.22-ga',
49    'jdk17u/jdk-17.0.10-ga',
50    'jdk21u/jdk-21.0.2-ga',
51]
52
53
54def error_and_exit(msg: str) -> None:
55  print(f'Error: {msg}', file=sys.stderr)
56  sys.exit(1)
57
58
59def get_commit_or_exit(git_ref: str) -> Commit:
60  try:
61    return LIBCORE_REPO.commit(git_ref)
62  except BadName as e:
63    error_and_exit(f'{e}')
64
65
66def autocomplete_tag_or_commit(str_tag_or_commit: str) -> List[str]:
67  """Returns a list of tags / commits matching the given partial string."""
68  if str_tag_or_commit is None:
69    str_tag_or_commit = ''
70  return list(
71      filter(lambda tag: tag.startswith(str_tag_or_commit), AUTOCOMPLETE_TAGS))
72
73
74COMMAND_ACTIONS = ['add', 'modify', 'remove', 'sort']
75
76
77def autocomplete_action(partial_str: str) -> None:
78  result_list = list(
79      filter(lambda action: action.startswith(partial_str), COMMAND_ACTIONS))
80  print('\n'.join(result_list))
81  exit(0)
82
83
84def main(argv: Sequence[str]) -> None:
85  is_auto_complete = len(argv) >= 2 and argv[0] == '--autocomplete'
86  # argparse can't help autocomplete subcommand. We implement this without
87  # argparse here.
88  if is_auto_complete and argv[1] == '1':
89    action = argv[2] if len(argv) >= 3 else ''
90    autocomplete_action(action)
91
92  # If it's for autocompletion, then all arguments are optional.
93  parser_nargs = '?' if is_auto_complete else 1
94
95  main_parser = argparse.ArgumentParser(
96      description='A command line tool modifying the EXPECTED_UPSTREAM file.')
97  # --autocomplete <int> is an 'int' argument because the value represents
98  # the raw index of the argument to be autocompleted received in the Shell,
99  # and this number is not always the same as the number of arguments
100  # received here, i.e. len(argv), for examples of empty value in the
101  # argument or autocompleting the middle argument, not last argument.
102  main_parser.add_argument(
103      '--autocomplete', type=int, help='flag when tabbing in command line')
104  subparsers = main_parser.add_subparsers(
105      dest='command', help='sub-command help')
106
107  add_parser = subparsers.add_parser(
108      'add', help='Add a new entry into the EXPECTED_UPSTREAM '
109      'file')
110  add_parser.add_argument(
111      'tag_or_commit',
112      nargs=parser_nargs,
113      help='A git tag or commit in the upstream-openjdkXXX branch')
114  add_parser.add_argument(
115      'class_or_source_file',
116      nargs=parser_nargs,
117      help='Fully qualified class name or upstream source path')
118  add_parser.add_argument(
119      'ojluni_path', nargs='?', help='Destination path in ojluni/')
120
121  modify_parser = subparsers.add_parser(
122      'modify', help='Modify an entry in the EXPECTED_UPSTREAM file')
123  modify_parser.add_argument(
124      'class_or_ojluni_path', nargs=parser_nargs, help='File path in ojluni/')
125  modify_parser.add_argument(
126      'tag_or_commit',
127      nargs=parser_nargs,
128      help='A git tag or commit in the upstream-openjdkXXX branch')
129  modify_parser.add_argument(
130      'source_file', nargs='?', help='A upstream source path')
131
132  modify_parser = subparsers.add_parser(
133      'remove', help='Remove an entry in the EXPECTED_UPSTREAM file')
134  modify_parser.add_argument(
135      'class_or_ojluni_path', nargs=parser_nargs, help='File path in ojluni/')
136
137  subparsers.add_parser(
138      'sort', help='Sort the entries in the EXPECTED_UPSTREAM file')
139
140  args = main_parser.parse_args(argv)
141
142  expected_upstream_file = ExpectedUpstreamFile()
143  expected_entries = expected_upstream_file.read_all_entries()
144
145  if is_auto_complete:
146    no_args = args.autocomplete
147
148    autocomp_result = []
149    if args.command == 'modify' or args.command == 'remove':
150      if no_args == 2:
151        input_class_or_ojluni_path = args.class_or_ojluni_path
152        if input_class_or_ojluni_path is None:
153          input_class_or_ojluni_path = ''
154
155        existing_dst_paths = list(
156            map(lambda entry: entry.dst_path, expected_entries))
157        ojluni_finder: OjluniFinder = OjluniFinder(existing_dst_paths)
158        # Case 1: Treat the input as file path
159        autocomp_result += ojluni_finder.match_path_prefix(
160            input_class_or_ojluni_path)
161
162        # Case 2: Treat the input as java package / class name
163        autocomp_result += ojluni_finder.match_classname_prefix(
164            input_class_or_ojluni_path)
165      elif no_args == 3 and args.command == 'modify':
166        autocomp_result += autocomplete_tag_or_commit(args.tag_or_commit)
167    elif args.command == 'add':
168      if no_args == 2:
169        autocomp_result += autocomplete_tag_or_commit(args.tag_or_commit)
170      elif no_args == 3:
171        commit = get_commit_or_exit(args.tag_or_commit)
172        class_or_src_path = args.class_or_source_file
173        if class_or_src_path is None:
174          class_or_src_path = ''
175
176        openjdk_finder: OpenjdkFinder = OpenjdkFinder(commit)
177
178        matches = openjdk_finder.match_path_prefix(
179            class_or_src_path)
180
181        matches += openjdk_finder.match_classname_prefix(
182            class_or_src_path)
183
184        existing_dst_paths = set(map(lambda e: e.dst_path, expected_entries))
185
186        # Translate the class names or source paths to dst paths and exclude
187        # such matches from the auto-completion result
188        def source_not_exists(src_path_or_class: str) -> bool:
189          nonlocal existing_dst_paths, openjdk_finder
190          t_src_path = openjdk_finder.find_src_path_from_classname(
191              src_path_or_class)
192          if t_src_path is None:
193            # t_src_path is a java package. It must not in existing_dst_paths.
194            return True
195          t_dst_path = OpenjdkFinder.translate_src_path_to_ojluni_path(
196              t_src_path)
197          return t_dst_path not in existing_dst_paths
198
199        autocomp_result += list(filter(source_not_exists, matches))
200
201    print('\n'.join(autocomp_result))
202    exit(0)
203
204  if args.command == 'modify':
205    dst_class_or_file = args.class_or_ojluni_path[0]
206    dst_path = OjluniFinder.translate_from_class_name_to_ojluni_path(
207        dst_class_or_file)
208    matches = list(filter(lambda e: dst_path == e.dst_path, expected_entries))
209    if not matches:
210      error_and_exit(f'{dst_path} is not found in the EXPECTED_UPSTREAM.')
211    entry: ExpectedUpstreamEntry = matches[0]
212    str_tag_or_commit = args.tag_or_commit[0]
213    is_src_given = args.source_file is not None
214    src_path = args.source_file if is_src_given else entry.src_path
215    commit = get_commit_or_exit(str_tag_or_commit)
216    openjdk_finder: OpenjdkFinder = OpenjdkFinder(commit)
217    if openjdk_finder.has_file(src_path):
218      pass
219    elif not is_src_given:
220      guessed_src_path = openjdk_finder.find_src_path_from_ojluni_path(dst_path)
221      if guessed_src_path is None:
222        error_and_exit('[source_file] argument is required.')
223      src_path = guessed_src_path
224    else:
225      error_and_exit(f'{src_path} is not found in the {str_tag_or_commit}')
226    entry.git_ref = str_tag_or_commit
227    entry.src_path = src_path
228    expected_upstream_file.write_all_entries(expected_entries)
229    print(f'Modified the entry {entry}')
230  elif args.command == 'remove':
231    dst_class_or_file = args.class_or_ojluni_path[0]
232    dst_path = OjluniFinder.translate_from_class_name_to_ojluni_path(
233        dst_class_or_file)
234    matches = list(filter(lambda e: dst_path == e.dst_path, expected_entries))
235    if not matches:
236      error_and_exit(f'{dst_path} is not found in the EXPECTED_UPSTREAM.')
237
238    entry: ExpectedUpstreamEntry = matches[0]
239    expected_entries.remove(entry)
240    expected_upstream_file.write_all_entries(expected_entries)
241    print(f'Removed the entry {entry}')
242  elif args.command == 'add':
243    class_or_src_path = args.class_or_source_file[0]
244    str_tag_or_commit = args.tag_or_commit[0]
245    commit = get_commit_or_exit(str_tag_or_commit)
246    openjdk_finder = OpenjdkFinder(commit)
247    src_path = openjdk_finder.find_src_path_from_classname(class_or_src_path)
248    if src_path is None:
249      search_paths = openjdk_finder.get_search_paths()
250      error_and_exit(f'{class_or_src_path} is not found in {commit}. '
251                     f'The search paths are:\n{search_paths}')
252    ojluni_path = args.ojluni_path
253    # Guess the source path if it's not given in the argument
254    if ojluni_path is None:
255      ojluni_path = OpenjdkFinder.translate_src_path_to_ojluni_path(src_path)
256    if ojluni_path is None:
257      error_and_exit('The ojluni destination path is not given.')
258
259    matches = list(
260        filter(lambda e: ojluni_path == e.dst_path, expected_entries))
261    if matches:
262      error_and_exit(f"Can't add the file {ojluni_path} because "
263                     f'{class_or_src_path} exists in the EXPECTED_UPSTREAM')
264
265    new_entry = ExpectedUpstreamEntry(ojluni_path, str_tag_or_commit, src_path)
266    expected_upstream_file.write_new_entry(new_entry, expected_entries)
267  elif args.command == 'sort':
268    expected_upstream_file.sort_and_write_all_entries(expected_entries)
269  else:
270    error_and_exit(f'Unknown subcommand: {args.command}')
271
272
273if __name__ == '__main__':
274  main(sys.argv[1:])
275