git.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. # Copyright 2019 Google LLC
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. import os
  15. import subprocess
  16. from lib import command_trace
  17. from lib import source
  18. def find_changed_or_files(all, rev_or_files, patterns):
  19. """Finds files.
  20. Args:
  21. all: Force finding all files.
  22. rev_or_files: A single revision, a list of files, or empty.
  23. patterns: A list of git matching patterns
  24. Returns:
  25. Files that match.
  26. If rev_or_files is a single revision, the result is all files that match
  27. the patterns that have changed since the revision.
  28. If rev_or_files is a list of files, the result is all the files that match
  29. that list of files. The files can be patterns.
  30. If rev_or_files is empty, the result is all the files that match patterns.
  31. """
  32. if all:
  33. return find_files(patterns)
  34. if not rev_or_files:
  35. return find_changed('origin/master', patterns)
  36. if len(rev_or_files) == 1 and is_revision(rev_or_files[0]):
  37. return find_changed(rev_or_files[0], patterns)
  38. else:
  39. return find_files(rev_or_files)
  40. def is_revision(word):
  41. """Returns true if the given word is a revision name according to git."""
  42. command = ['git', 'rev-parse', word, '--']
  43. with open(os.devnull, 'w') as dev_null:
  44. command_trace.log(command)
  45. rc = subprocess.call(command, stdout=dev_null, stderr=dev_null)
  46. return rc == 0
  47. def find_changed(revision, patterns):
  48. """Finds files changed since a revision."""
  49. # Always include -- indicate that revision is known to be a revision, even
  50. # if no patterns follow.
  51. command = ['git', 'diff', '-z', '--name-only', '--diff-filter=ACMR',
  52. revision, '--']
  53. command.extend(patterns)
  54. command.extend(standard_exclusions())
  55. return _null_split_output(command)
  56. def find_files(patterns=None):
  57. """Finds files matching the given patterns using git ls-files."""
  58. command = ['git', 'ls-files', '-z', '--']
  59. if patterns:
  60. command.extend(patterns)
  61. command.extend(standard_exclusions())
  62. return _null_split_output(command)
  63. def find_lines_matching(pattern, sources=None):
  64. command = [
  65. 'git', 'grep',
  66. '-n', # show line numbers
  67. '-I', # exclude binary files
  68. pattern,
  69. '--'
  70. ]
  71. if sources:
  72. command.extend(sources)
  73. command.extend(standard_exclusions())
  74. command_trace.log(command)
  75. bufsize = 4096
  76. proc = subprocess.Popen(command, bufsize=bufsize, stdout=subprocess.PIPE)
  77. result = []
  78. try:
  79. while proc.poll() is None:
  80. result.append(proc.stdout.read(bufsize))
  81. except KeyboardInterrupt:
  82. proc.terminate()
  83. proc.wait()
  84. return b''.join(result).decode('utf8', errors='replace')
  85. def make_patterns(dirs):
  86. """Returns a list of git match patterns for the given directories."""
  87. return ['%s/**' % d for d in dirs]
  88. def make_exclusions(dirs):
  89. return [':(exclude)' + d for d in dirs]
  90. def standard_exclusions():
  91. result = make_exclusions(source.IGNORE)
  92. result.append(':(exclude)**/third_party/**')
  93. return result
  94. def is_within_repo():
  95. """Returns whether the current working directory is within a git repo."""
  96. try:
  97. subprocess.check_output(['git', 'status'])
  98. return True
  99. except subprocess.CalledProcessError:
  100. return False
  101. def get_repo_root():
  102. """Returns the absolute path to the root of the current git repo."""
  103. command = ['git', 'rev-parse', '--show-toplevel']
  104. return subprocess.check_output(command, text=True, errors='replace').rstrip()
  105. def _null_split_output(command):
  106. """Runs the given command and splits its output on the null byte."""
  107. command_trace.log(command)
  108. result = subprocess.check_output(command, text=True, errors='replace')
  109. return [name for name in result.rstrip().split('\0') if name]