def explode(self, commits, deps_from, deps_on): """ Walk the dependency tree breadth-first starting with the leaves at the bottom. For each commit, figure out whether it should be exploded :param commits: dict mapping SHA1 hashes to pygit2.Commit objects :param deps_from: dict mapping dependents to dependencies :param deps_on: dict mapping in opposite direction """ todo = self.get_leaves(commits, deps_from) # Each time we explode a commit, we'll remove it from any # dict which is a value of this dict. unexploded_deps_from = copy.deepcopy(deps_from) self.logger.debug("Initial queue of leaves:") for commit in todo: self.logger.debug(' ' + GitUtils.commit_summary(commit)) self.current_branch = None while todo: commit = todo.pop(0) sha = commit.hex self.logger.debug("Exploding %s" % GitUtils.commit_summary(commit)) if unexploded_deps_from[sha]: abort("BUG: unexploded deps from %s" % GitUtils.commit_summary(commit)) deps = deps_from[sha] self.prepare_cherrypick_base(sha, deps, commits) self.cherry_pick(sha) self.queue_new_leaves(todo, commit, commits, deps_on, unexploded_deps_from)
def __init__(self, repo, base, head, debug, context_lines): self.logger = standard_logger('git-explode', debug) self.debug = debug self.repo = repo self.base = base self.base_commit = GitUtils.ref_commit(repo, base) self.logger.debug("base commit %s is %s" % (base, GitUtils.commit_summary(self.base_commit))) self.head = head self.context_lines = context_lines self.topic_mgr = TopicManager('topic%d', self.logger) # Map commits to their exploded version self.exploded = {}
def queue_new_leaves(self, todo, exploded_commit, commits, deps_on, unexploded_deps_from): """When a commit is exploded, there may be other commits in the dependency tree which only had a single dependency on this commit. In that case they have effectively become leaves on the dependency tree of unexploded commits, so they should be added to the explode queue. """ sha1 = exploded_commit.hex for dependent in deps_on[sha1]: del unexploded_deps_from[dependent][sha1] if not unexploded_deps_from[dependent]: new = commits[dependent] self.logger.debug("+ pushed to queue: %s" % GitUtils.commit_summary(new)) todo.insert(0, new)
def register_new_dependent(self, dependent, dependent_sha1): if dependent_sha1 not in self.dependencies: self.logger.info(" New dependent: %s" % GitUtils.commit_summary(dependent)) self.dependencies[dependent_sha1] = {} self.notify_listeners("new_dependent", dependent)
def blame_hunk(self, dependent, parent, path, hunk): """Run git blame on the parts of the hunk which exist in the older commit in the diff. The commits generated by git blame are the commits which the newer commit in the diff depends on, because without the lines from those commits, the hunk would not apply correctly. """ line_range_before = "-%d,%d" % (hunk.old_start, hunk.old_lines) line_range_after = "+%d,%d" % (hunk.new_start, hunk.new_lines) self.logger.debug(" Blaming hunk %s @ %s" % (line_range_before, parent.hex[:8])) if not self.tree_lookup(path, parent): # This is probably because dependent added a new directory # which was not previously in the parent. return cmd = [ 'git', 'blame', '--porcelain', '-L', "%d,+%d" % (hunk.old_start, hunk.old_lines), parent.hex, '--', path ] blame = subprocess.check_output(cmd, universal_newlines=True) dependent_sha1 = dependent.hex if dependent_sha1 not in self.dependencies: self.logger.debug(" New dependent: %s" % GitUtils.commit_summary(dependent)) self.dependencies[dependent_sha1] = {} self.notify_listeners("new_dependent", dependent) line_to_culprit = {} for line in blame.split('\n'): self.logger.debug(" !" + line.rstrip()) m = re.match('^([0-9a-f]{40}) (\d+) (\d+)( \d+)?$', line) if not m: continue dependency_sha1, orig_line_num, line_num = m.group(1, 2, 3) line_num = int(line_num) dependency = self.get_commit(dependency_sha1) line_to_culprit[line_num] = dependency.hex if self.is_excluded(dependency): self.logger.debug( " Excluding dependency %s from line %s (%s)" % (dependency_sha1[:8], line_num, GitUtils.oneline(dependency))) continue if dependency_sha1 not in self.dependencies[dependent_sha1]: if not self.seen_commit(dependency): self.notify_listeners("new_commit", dependency) self.dependencies[dependent_sha1][dependency_sha1] = {} self.notify_listeners("new_dependency", dependent, dependency, path, line_num) self.logger.debug( " New dependency %s -> %s via line %s (%s)" % (dependent_sha1[:8], dependency_sha1[:8], line_num, GitUtils.oneline(dependency))) if dependency_sha1 in self.todo_d: self.logger.debug( " Dependency on %s via line %s already in TODO" % ( dependency_sha1[:8], line_num, )) continue if dependency_sha1 in self.done_d: self.logger.debug( " Dependency on %s via line %s already done" % ( dependency_sha1[:8], line_num, )) continue if dependency_sha1 not in self.dependencies: if self.options.recurse: self.todo.append(dependency) self.todo_d[dependency.hex] = True self.logger.debug(" + Added %s to TODO" % dependency.hex[:8]) dep_sources = self.dependencies[dependent_sha1][dependency_sha1] if path not in dep_sources: dep_sources[path] = {} self.notify_listeners('new_path', dependent, dependency, path, line_num) if line_num in dep_sources[path]: abort("line %d already found when blaming %s:%s\n" "old:\n %s\n" "new:\n %s" % (line_num, parent.hex[:8], path, dep_sources[path][line_num], line)) dep_sources[path][line_num] = line self.logger.debug(" New line for %s -> %s: %s" % (dependent_sha1[:8], dependency_sha1[:8], line)) self.notify_listeners('new_line', dependent, dependency, path, line_num) diff_format = ' |%8.8s %5s %s%s' hunk_header = '@@ %s %s @@' % (line_range_before, line_range_after) self.logger.debug(diff_format % ('--------', '-----', '', hunk_header)) line_num = hunk.old_start for line in hunk.lines: if "\n\\ No newline at end of file" == line.content.rstrip(): break if line.origin == '+': rev = ln = '' else: rev = line_to_culprit[line_num] ln = line_num line_num += 1 self.logger.debug(diff_format % (rev, ln, line.origin, line.content.rstrip()))