def render(db, target, title, branch=None, commits=None, columns=DEFAULT_COLUMNS, title_right=None, listed_commits=None, rebases=None, branch_name=None, bottom_right=None, review=None, highlight=None, profiler=None, collapsable=False, user=None, extra_commits=None): addResources(target) if not profiler: profiler = Profiler() profiler.check("log: start") if branch is not None: repository = branch.repository commits = branch.getCommits(db)[:] commit_set = log.commitset.CommitSet(commits) else: assert commits is not None repository = commits[0].repository if len(commits) else None commit_set = log.commitset.CommitSet(commits) profiler.check("log: commits") heads = commit_set.getHeads() tails = commit_set.getTails() rebase_old_heads = set() if rebases: class Rebase(object): def __init__(self, rebase_id, old_head, new_head, user, new_upstream, equivalent_merge, replayed_rebase, target_branch_name): self.id = rebase_id self.old_head = equivalent_merge or old_head self.new_head = new_head self.user = user self.new_upstream = new_upstream self.equivalent_merge = equivalent_merge self.replayed_rebase = replayed_rebase self.target_branch_name = target_branch_name # The first element in the tuples in 'rebases' is the rebase id, which # is an ever-increasing serial number that we can use as an indication # of the order in which the rebases were made. rebases = [Rebase(*rebase) for rebase in sorted(rebases)] rebase_old_heads = set(rebase.old_head for rebase in rebases) heads -= rebase_old_heads assert 0 <= len(heads) <= 1 if not heads: heads = set([rebases[-1].new_head]) if repository: target.addInternalScript(repository.getJS()) processed = set() summaries = {} for commit in commits: summary = commit.summary().strip() summaries[summary] = commit summaries[commit.sha1] = commit if extra_commits: for commit in extra_commits: summary = commit.summary().strip() summaries[summary] = commit summaries[commit.sha1] = commit def isFixupOrSquash(commit): key, _, summary = commit.summary().partition(" ") if key in ("fixup!", "squash!"): what = key[:-1] else: return None summary = summary.strip() commit = summaries.get(summary) if not commit and re.match("[0-9A-Fa-f]{40}$", summary): commit = summaries.get(summary) if not commit: try: sha1 = repository.revparse(summary) commit = Commit.fromSHA1(db, repository, sha1) except Exception: pass if commit: summary = commit.summary() return what, summary for width, column in columns: if isinstance(column, SummaryColumn): column.isFixupOrSquash = isFixupOrSquash break def output(table, commit, overrides={}): if commit not in processed: classes = ["commit"] row_id = None if len(commit.parents) > 1: classes.append("merge") if highlight == commit: classes.append("highlight") row_id = commit.sha1 if review: overrides["review"] = review row = table.tr(" ".join(classes), id=row_id) profiler.check("log: rendering: row") for index, (width, column) in enumerate(columns): column.render(db, commit, row.td(column.className(db, commit)), overrides=overrides) profiler.check("log: rendering: column %d" % (index + 1)) processed.add(commit) return row else: return None cursor = db.cursor() def emptyChangeset(child, parent=None): if parent is None: cursor.execute( """SELECT 1 FROM fileversions JOIN changesets ON (changesets.id=fileversions.changeset) JOIN reviewchangesets ON (reviewchangesets.changeset=changesets.id) WHERE changesets.child=%s AND reviewchangesets.review=%s""", (child.getId(db), review.id)) else: cursor.execute( """SELECT 1 FROM fileversions JOIN changesets ON (changesets.id=fileversions.changeset) JOIN reviewchangesets ON (reviewchangesets.changeset=changesets.id) WHERE changesets.parent=%s AND changesets.child=%s AND reviewchangesets.review=%s""", (parent.getId(db), child.getId(db), review.id)) return not cursor.fetchone() def inner(target, head, tails, align='right', title=None, table=None, silent_if_empty=set(), upstream=None): if not table: table = target.table('log', align=align, cellspacing=0) for width, column in columns: table.col(width=('%d%%' % width)) if title: thead = table.thead() row = thead.tr("title") header = row.td("h1", colspan=len(columns)).h1() header.text(title) if callable(title_right): title_right(db, header.span("right")) row = thead.tr('headings') for width, column in columns: column.heading(row.td(column.className(db, None))) elif head is None or head in tails: if upstream: tag = upstream.findInterestingTag(db) if tag: what = tag else: what = upstream.sha1[:8] message = "Merged with base branch (%s)." % what else: message = "Merged with base branch." thead = table.thead() row = thead.tr('basemerge') row.td(colspan=len(columns), align='center').text(message) return (None, None, None, False) tbody = table.tbody() commit = head last_commit = None skipped = True while commit and commit not in tails: suppress = False optional_merge = False listed = listed_commits is None or commit.getId( db) in listed_commits if commit in silent_if_empty and emptyChangeset(commit): # This is a clean automatically generated merge commit; pretend it isn't here at all. suppress = True if not suppress and not listed: suppress = len(commit.parents) == 1 optional_merge = not suppress if not suppress: commit_tr = output(tbody, commit) else: commit_tr = None if listed: skipped = False last_commit = commit if len(commit.parents) == 0: break elif len(commit.parents) == 1: commit = commit_set.get(commit.parents[0]) elif len(commit.parents) > 1: if len(commit.parents) > 2: common_ancestors = commit_set.getCommonAncestors(commit) match = re_octopus.match(commit.message.split("\n", 1)[0]) if match: titles = re.findall("'([^']+)'", match.group(1)) if len(titles) != len(commit.parents): titles = None else: titles = None for index, sha1 in enumerate(commit.parents): if sha1 in commit_set: sublog = tbody.tr('sublog') inner_last_commit, inner_table, inner_tail, inner_skipped = inner( sublog.td(colspan=len(columns)), commit_set[sha1], common_ancestors, title=titles and titles[index] or None) if inner_skipped: sublog.remove() if optional_merge and commit_tr: commit_tr.remove() if not common_ancestors: return (None, None, None, False) commit = common_ancestors.pop() continue parent1_sha1 = commit.parents[0] parent2_sha1 = commit.parents[1] parent1 = commit_set.get(parent1_sha1) parent2 = commit_set.get(parent2_sha1) # TODO: Try to remember what this code actually does, and why... if parent1_sha1 in rebase_old_heads: if parent2: sublog = tbody.tr('sublog') inner_last_commit, inner_table, inner_tail, inner_skipped = \ inner(sublog.td(colspan=len(columns)), parent2, tails) if inner_skipped: sublog.remove() if optional_merge and commit_tr: commit_tr.remove() return (commit, table, parent1_sha1, False) elif parent2_sha1 in rebase_old_heads: if parent1: sublog = tbody.tr('sublog') inner_last_commit, inner_table, inner_tail, inner_skipped = \ inner(sublog.td(colspan=len(columns)), parent1, tails) if inner_skipped: sublog.remove() if optional_merge and commit_tr: commit_tr.remove() return (commit, table, parent2_sha1, False) if parent1 and parent2: common_ancestors = commit_set.getCommonAncestors(commit) merged_remote_into_local = re_remote_into_local.match( commit.summary()) or re_side_into_main.match( commit.summary()) def rankPaths(commit, tails): shortest = None shortest_length = len(commit_set) longest = None longest_length = 0 for sha1 in commit.parents: parent = commit_set[sha1] counted = set() pending = set([parent]) while pending: candidate = pending.pop() if candidate in counted: continue if candidate in tails: continue counted.add(candidate) pending.update( commit_set.getParents(candidate)) length = len(counted) if length < shortest_length: shortest = parent shortest_length = length if length >= longest_length: longest = parent longest_length = length return shortest, shortest_length, longest, longest_length show_merged, shortest_length, show_normal, longest_length = rankPaths( commit, common_ancestors | tails) display_parallel = False if merged_remote_into_local and shortest_length * 2 > longest_length: if len(common_ancestors) == 1 and len( commit_set.filtered([commit]).getTails()) == 1: display_parallel = True else: show_merged = parent2 show_normal = parent1 if display_parallel: all_empty = True for sha1 in commit.parents: sublog = tbody.tr('sublog') inner_last_commit, inner_table, inner_tail, inner_skipped = inner( sublog.td(colspan=len(columns)), commit_set[sha1], common_ancestors | tails) if inner_skipped: sublog.remove() else: all_empty = False if all_empty and optional_merge and commit_tr: commit_tr.remove() commit = common_ancestors.pop() else: sublog = tbody.tr('sublog') inner_last_commit, inner_table, inner_tail, inner_skipped = inner( sublog.td(colspan=len(columns)), show_merged, common_ancestors | tails) if inner_skipped: sublog.remove() if optional_merge and commit_tr: commit_tr.remove() commit = show_normal else: if parent1: upstream_sha1 = parent2_sha1 else: upstream_sha1 = parent1_sha1 if not commit in silent_if_empty: # Merge with the base branch. inner(tbody.tr('sublog').td(colspan=len(columns)), None, None, upstream=Commit.fromSHA1(db, repository, upstream_sha1)) if parent1: commit = parent1 else: commit = parent2 return (last_commit, table, last_commit.parents[0] if last_commit and last_commit.parents else None, skipped) class_name = "paleyellow log" if collapsable: class_name += " collapsable" table = target.table(class_name, align='center', cellspacing=0) for width, column in columns: table.col(width=('%d%%' % width)) thead = table.thead("title") row = thead.tr("title") header = row.td("h1", colspan=len(columns)).h1() error_message = None if len(commit_set) == 0: thead = table.thead() row = thead.tr('error') cell = row.td(colspan=len(columns), align='center') cell.text("No commits. ") if review: cell.a(href="showtree?sha1=%s&review=%d" % (review.branch.head_sha1, review.id)).text("[Browse tree]") return elif len(heads) > 1: error_message = "Invalid commit set: Multiple heads." elif len(heads) == 0: error_message = "Invalid commit set: No heads." if error_message is not None: thead = table.thead() row = thead.tr('error') cell = row.td(colspan=len(columns), align='center') cell.text(error_message) return head = heads.pop() if heads else None row = thead.tr('headings') for width, column in columns: column.heading(row.td(column.className(db, None))) first_rebase = True silent_if_empty = set() if rebases: for rebase in rebases: if rebase.equivalent_merge: silent_if_empty.add(rebase.equivalent_merge) top_rebases = [] while rebases and head == rebases[-1].new_head: rebase = rebases.pop() top_rebases.append((head, rebase)) head = rebase.old_head for rebase_head, rebase in top_rebases: thead = table.thead("rebase") row = thead.tr('rebase') cell = row.td(colspan=len(columns), align='center') if rebase.new_upstream is None and not rebase.target_branch_name: cell.text("History rewritten") else: cell.text("Branch rebased onto ") if rebase.target_branch_name: anchor = cell.a( href=("/checkbranch?repository=%d&commit=%s" % (repository.id, rebase.target_branch_name))) anchor.text(rebase.target_branch_name) else: upstream_description = repository.describe( db, rebase.new_upstream.sha1) if upstream_description is None: upstream_description = rebase.new_upstream.sha1[:8] anchor = cell.a( href="/%s/%s" % (repository.name, rebase.new_upstream.sha1)) anchor.text(upstream_description) cell.text(" by %s" % rebase.user.fullname) if first_rebase: cell.text(": ") review_param = "&review=%d" % review.id if review else "" cell.a(href="log?repository=%d&branch=%s%s" % (repository.id, branch_name, review_param)).text("[actual log]") if user and user == rebase.user: cell.text(" ") cell.a(href="javascript:revertRebase(%d)" % rebase.id).text("[revert]") first_rebase = False else: cell.text(".") if rebase.replayed_rebase and not emptyChangeset( parent=rebase.replayed_rebase, child=rebase.new_head): output(table, rebase.new_head, overrides={ "type": "Rebase", "summary": "Changes introduced by rebase", "summary_classnames": ["rebase"], "author": rebase.user, "replayed_rebase": rebase.replayed_rebase }) while True: # 'local_tails' is the set of commits that, when reached, should make # inner() stop outputting commits and instead return. This set of # commits contains all the "tails" of the whole commit-set we're # rendering (in the 'tails' set here), as well as the "new head" of the # next rebase to be output. local_tails = tails.copy() if rebases: local_tails.add(rebases[-1].new_head) last_commit, table, tail, skipped = inner(target, head, local_tails, 'center', title, table, silent_if_empty) if rebases: rebase = rebases.pop() assert tail == rebase.new_head, "tail (%s) != rebase.new_head (%s)" % ( tail, rebase.new_head) while True: head = rebase.old_head thead = table.thead("rebase") row = thead.tr('rebase') cell = row.td(colspan=len(columns), align='center') if rebase.new_upstream is None and not rebase.target_branch_name: cell.text("History rewritten") else: cell.text("Branch rebased onto ") if rebase.target_branch_name: anchor = cell.a( href=("/checkbranch?repository=%d&commit=%s" % (repository.id, rebase.target_branch_name))) anchor.text(rebase.target_branch_name) else: upstream_description = repository.describe( db, rebase.new_upstream.sha1) if upstream_description is None: upstream_description = rebase.new_upstream.sha1[:8] anchor = cell.a( href="/%s/%s" % (repository.name, rebase.new_upstream.sha1)) anchor.text(upstream_description) cell.text(" by %s" % rebase.user.fullname) if first_rebase: cell.text(": ") review_param = "&review=%d" % review.id if review else "" cell.a(href="log?repository=%d&branch=%s%s" % (repository.id, branch_name, review_param)).text("[actual log]") first_rebase = False else: cell.text(".") if rebase.replayed_rebase and not emptyChangeset( parent=rebase.replayed_rebase, child=rebase.new_head): output(table, rebase.new_head, overrides={ "type": "Rebase", "summary": "Changes introduced by rebase", "summary_classnames": ["rebase"], "author": rebase.user, "replayed_rebase": rebase.replayed_rebase }) if rebases and rebases[-1].new_head == head: rebase = rebases.pop() else: break continue if last_commit: if len(last_commit.parents) == 1: upstream = Commit.fromSHA1(db, repository, last_commit.parents[0]) upstream_description = repository.describe(db, upstream.sha1) if not upstream_description: upstream_description = upstream.sha1[:8] row = table.thead("rebase").tr('upstream') cell = row.td(colspan=len(columns), align='center') cell.text("Based on: ") anchor = cell.a(href="/%s/%s" % (repository.name, upstream.sha1)) anchor.text(upstream_description) if callable(bottom_right): bottom_right(db, table.tfoot().tr().td(colspan=len(columns))) break profiler.check("log: rendering") if "%d" in title: header.text(title % len(processed)) else: header.text(title) if callable(title_right): title_right(db, header.span("right"))
def render(db, target, title, branch=None, commits=None, columns=DEFAULT_COLUMNS, title_right=None, listed_commits=None, rebases=None, branch_name=None, bottom_right=None, review=None, highlight=None, profiler=None, collapsable=False, user=None, extra_commits=None): addResources(target) if not profiler: profiler = Profiler() profiler.check("log: start") if branch is not None: repository = branch.repository branch.loadCommits(db) commits = branch.commits[:] commit_set = log.commitset.CommitSet(branch.commits) else: assert commits is not None repository = commits[0].repository if len(commits) else None commit_set = log.commitset.CommitSet(commits) profiler.check("log: commits") heads = commit_set.getHeads() tails = commit_set.getTails() if rebases is not None: frebases = dict([(old_head, (rebase_id, new_head, rebase_user, new_upstream, target_branch_name)) for rebase_id, old_head, new_head, rebase_user, new_upstream, target_branch_name in rebases]) rrebases = dict([(new_head, (rebase_id, old_head, rebase_user, new_upstream, target_branch_name)) for rebase_id, old_head, new_head, rebase_user, new_upstream, target_branch_name in rebases]) for rebase_id, old_head, new_head, rebase_user, new_upstream, target_branch_name in rebases: if old_head in heads: next_head = new_head while next_head: for head in heads: if head == old_head: continue elif commit_set.isAncestorOf(next_head, head) or next_head in tails: heads.remove(old_head) next_head = None break else: next_head = frebases.get(next_head, (None, None))[1] else: frebases = None rrebases = None if repository: target.addInternalScript(repository.getJS()) processed = set() summaries = {} for commit in commits: summary = commit.summary().strip() summaries[summary] = commit if len(summary) > 60: summaries[summary[:40]] = commit summaries[commit.sha1] = commit summaries[commit.sha1[:8]] = commit if extra_commits: for commit in extra_commits: summary = commit.summary().strip() summaries[summary] = commit if len(summary) > 60: summaries[summary[:40]] = commit summaries[commit.sha1] = commit summaries[commit.sha1[:8]] = commit def isFixupOrSquash(commit): if commit.message.startswith("fixup! "): what = "fixup" summary = commit.summary()[6:].strip() elif commit.message.startswith("squash! "): what = "squash" summary = commit.summary()[7:].strip() else: return None commit = (summaries.get(summary) or summaries.get(summary[:40]) or summaries.get(summary[:8])) if commit: return what, commit else: return None for width, column in columns: if isinstance(column, SummaryColumn): column.isFixupOrSquash = isFixupOrSquash break def output(table, commit): if commit not in processed: classes = ["commit"] row_id = None if len(commit.parents) > 1: classes.append("merge") if highlight == commit: classes.append("highlight") row_id = commit.sha1 row = table.tr(" ".join(classes), id=row_id) profiler.check("log: rendering: row") for index, (width, column) in enumerate(columns): column.render(db, commit, row.td(column.className(db, commit))) profiler.check("log: rendering: column %d" % (index + 1)) processed.add(commit) return row else: return None cursor = db.cursor() def inner(target, head, tails, align='right', title=None, table=None, silent_merges=set(), upstream=None): if not table: table = target.table('log', align=align, cellspacing=0) for width, column in columns: table.col(width=('%d%%' % width)) if title: thead = table.thead() row = thead.tr("title") header = row.td("h1", colspan=len(columns)).h1() header.text(title) if callable(title_right): title_right(db, header.span("right")) row = thead.tr('headings') for width, column in columns: column.heading(row.td(column.className(db, None))) elif head is None or head in tails: if upstream: tag = upstream.findInterestingTag(db) if tag: what = tag else: what = upstream.sha1[:8] message = "Merged with base branch (%s)." % what else: message = "Merged with base branch." thead = table.thead() row = thead.tr('basemerge') row.td(colspan=len(columns), align='center').text(message) return (None, None, None, False) tbody = table.tbody() commit = head sha1s = [] last_commit = None skipped = True while commit and commit not in tails: suppress = False optional_merge = False listed = listed_commits is None or commit.getId(db) in listed_commits if len(commit.parents) > 1 and commit in silent_merges: cursor.execute("""SELECT 1 FROM fileversions JOIN changesets ON (changesets.id=fileversions.changeset) JOIN reviewchangesets ON (reviewchangesets.changeset=changesets.id) WHERE changesets.child=%s AND reviewchangesets.review=%s""", (commit.getId(db), review.id)) if not cursor.fetchone(): # This is a clean automatically generated merge commit; pretend it isn't here at all. suppress = True if not suppress and not listed: suppress = len(commit.parents) == 1 optional_merge = not suppress if not suppress: commit_tr = output(tbody, commit) else: commit_tr = None if listed: skipped = False last_commit = commit if len(commit.parents) == 0: break elif len(commit.parents) == 1: commit = commit_set.get(commit.parents[0]) elif len(commit.parents) > 1: if len(commit.parents) > 2: common_ancestors = commit_set.getCommonAncestors(commit) match = re_octopus.match(commit.message.split("\n", 1)[0]) if match: titles = re.findall("'([^']+)'", match.group(1)) if len(titles) != len(commit.parents): titles = None else: titles = None for index, sha1 in enumerate(commit.parents): if sha1 in commit_set: sublog = tbody.tr('sublog') inner_last_commit, inner_table, inner_tail, inner_skipped = inner(sublog.td(colspan=len(columns)), commit_set[sha1], common_ancestors, title=titles and titles[index] or None) if inner_skipped: sublog.remove() if optional_merge and commit_tr: commit_tr.remove() if not common_ancestors: return (None, None, None, False) commit = common_ancestors.pop() continue parent1_sha1 = commit.parents[0] parent2_sha1 = commit.parents[1] parent1 = commit_set.get(parent1_sha1) parent2 = commit_set.get(parent2_sha1) if rrebases: if parent1_sha1 in rrebases: if parent2: sublog = tbody.tr('sublog') inner_last_commit, inner_table, inner_tail, inner_skipped = inner(sublog.td(colspan=len(columns)), parent2, tails) if inner_skipped: sublog.remove() if optional_merge and commit_tr: commit_tr.remove() return (commit, table, parent1_sha1, False) elif parent2_sha1 in rrebases: if parent1: sublog = tbody.tr('sublog') inner_last_commit, inner_table, inner_tail, inner_skipped = inner(sublog.td(colspan=len(columns)), parent1, tails) if inner_skipped: sublog.remove() if optional_merge and commit_tr: commit_tr.remove() return (commit, table, parent2_sha1, False) #raise Exception, repr([parent1_sha1, parent1, parent2_sha1, parent2]) if parent1 and parent2: common_ancestors = commit_set.getCommonAncestors(commit) merged_remote_into_local = re_remote_into_local.match(commit.summary()) or re_side_into_main.match(commit.summary()) def rankPaths(commit, tails): shortest = None shortest_length = len(commit_set) longest = None longest_length = 0 for sha1 in commit.parents: parent = commit_set[sha1] counted = set() pending = set([parent]) while pending: candidate = pending.pop() if candidate in counted: continue if candidate in tails: continue counted.add(candidate) pending.update(commit_set.getParents(candidate)) length = len(counted) if length < shortest_length: shortest = parent shortest_length = length if length >= longest_length: longest = parent longest_length = length return shortest, shortest_length, longest, longest_length show_merged, shortest_length, show_normal, longest_length = rankPaths(commit, common_ancestors | tails) display_parallel = False if merged_remote_into_local and shortest_length * 2 > longest_length: if len(common_ancestors) == 1 and len(commit_set.filtered([commit]).getTails()) == 1: display_parallel = True else: show_merged = parent2 show_normal = parent1 if display_parallel: all_empty = True for sha1 in commit.parents: sublog = tbody.tr('sublog') inner_last_commit, inner_table, inner_tail, inner_skipped = inner(sublog.td(colspan=len(columns)), commit_set[sha1], common_ancestors | tails) if inner_skipped: sublog.remove() else: all_empty = False if all_empty and optional_merge and commit_tr: commit_tr.remove() commit = common_ancestors.pop() else: sublog = tbody.tr('sublog') inner_last_commit, inner_table, inner_tail, inner_skipped = inner(sublog.td(colspan=len(columns)), show_merged, common_ancestors | tails) if inner_skipped: sublog.remove() if optional_merge and commit_tr: commit_tr.remove() commit = show_normal else: if parent1: upstream_sha1 = parent2_sha1 else: upstream_sha1 = parent1_sha1 if not commit in silent_merges: # Merge with the base branch. inner(tbody.tr('sublog').td(colspan=len(columns)), None, None, upstream=Commit.fromSHA1(db, repository, upstream_sha1)) if parent1: commit = parent1 else: commit = parent2 return (last_commit, table, last_commit.parents[0] if last_commit and last_commit.parents else None, skipped) class_name = "paleyellow log" if collapsable: class_name += " collapsable" table = target.table(class_name, align='center', cellspacing=0) for width, column in columns: table.col(width=('%d%%' % width)) thead = table.thead("title") row = thead.tr("title") header = row.td("h1", colspan=len(columns)).h1() error_message = None if len(commit_set) == 0: thead = table.thead() row = thead.tr('error') cell = row.td(colspan=len(columns), align='center') cell.text("No commits. ") if review: cell.a(href="showtree?sha1=%s&review=%d" % (review.branch.head.sha1, review.id)).text("[Browse tree]") return elif len(heads) > 1: error_message = "Invalid commit set: Multiple heads." elif len(heads) == 0: error_message = "Invalid commit set: No heads." if error_message is not None: thead = table.thead() row = thead.tr('error') cell = row.td(colspan=len(columns), align='center') cell.text(error_message) return head = heads.pop() if heads else None row = thead.tr('headings') for width, column in columns: column.heading(row.td(column.className(db, None))) first_rebase = True silent_merges = set() if frebases: rebase_head = head top_rebases = [] while rebase_head in frebases: top_rebases.insert(0, (rebase_head, frebases[rebase_head])) rebase_head = frebases[rebase_head][1] del rrebases[rebase_head] for rebase_head, rebase in top_rebases: thead = table.thead("rebase") row = thead.tr('rebase') cell = row.td(colspan=len(columns), align='center') new_upstream = rebase[3] if new_upstream is None and not rebase[4]: cell.text("History rewritten") else: cell.text("Branch rebased onto ") if rebase[4]: cell.a(href="/checkbranch?repository=%d&commit=%s" % (repository.id, rebase[4])).text(rebase[4]) else: tag = new_upstream.findInterestingTag(db) if tag: cell.text(tag) else: cell.a(href="/%s/%s" % (repository.name, new_upstream.sha1)).text(new_upstream.sha1[:8]) silent_merges.add(rebase_head) cell.text(" by %s" % rebase[2].fullname) if first_rebase: cell.text(": ") cell.a(href="log?repository=%d&branch=%s" % (repository.id, branch_name)).text("[actual log]") if rebase[0] is not None and user == rebase[2]: cell.text(" ") cell.a(href="javascript:revertRebase(%d)" % rebase[0]).text("[revert]") first_rebase = False else: cell.text(".") while True: last_commit, table, tail, skipped = inner(target, head, tails, 'center', title, table, silent_merges) if rrebases and last_commit: try: rebase = rrebases.pop(tail) except: break while True: head = rebase[1] thead = table.thead("rebase") row = thead.tr('rebase') cell = row.td(colspan=len(columns), align='center') new_upstream = rebase[3] if new_upstream is None and not rebase[4]: cell.text("History rewritten") else: cell.text("Branch rebased onto ") if rebase[4]: cell.a(href="/checkbranch?repository=%d&commit=%s" % (repository.id, rebase[4])).text(rebase[4]) else: tag = new_upstream.findInterestingTag(db) if tag: cell.text(tag) else: cell.a(href="/%s/%s" % (repository.name, new_upstream.sha1)).text(new_upstream.sha1[:8]) silent_merges.add(head) cell.text(" by %s" % rebase[2].fullname) if first_rebase: cell.text(": ") cell.a(href="log?repository=%d&branch=%s" % (repository.id, branch_name)).text("[actual log]") first_rebase = False else: cell.text(".") if head in rrebases: rebase = rrebases.pop(head) else: break continue if last_commit: if len(last_commit.parents) == 1: tag = repository.findInterestingTag(db, last_commit.parents[0]) if tag: thead = table.thead("rebase") row = thead.tr('upstream') cell = row.td(colspan=len(columns), align='center') cell.text("Based on: %s" % tag) if callable(bottom_right): bottom_right(db, table.tfoot().tr().td(colspan=len(columns))) break profiler.check("log: rendering") if "%d" in title: header.text(title % len(processed)) else: header.text(title) if callable(title_right): title_right(db, header.span("right"))
def render(db, target, title, branch=None, commits=None, columns=DEFAULT_COLUMNS, title_right=None, listed_commits=None, rebases=None, branch_name=None, bottom_right=None, review=None, highlight=None, profiler=None, collapsable=False, user=None, extra_commits=None, conflicts=set()): addResources(target) if not profiler: profiler = Profiler() profiler.check("log: start") if branch is not None: repository = branch.repository branch.loadCommits(db) commits = branch.commits[:] commit_set = log.commitset.CommitSet(branch.commits) else: assert commits is not None repository = commits[0].repository if len(commits) else None commit_set = log.commitset.CommitSet(commits) profiler.check("log: commits") heads = commit_set.getHeads() tails = commit_set.getTails() rebase_old_heads = set() if rebases: class Rebase(object): def __init__(self, rebase_id, old_head, new_head, user, new_upstream, target_branch_name): self.id = rebase_id self.old_head = Commit.fromId(db, repository, old_head) self.new_head = Commit.fromId(db, repository, new_head) self.user = user self.new_upstream = new_upstream and Commit.fromId(db, repository, new_upstream) self.target_branch_name = target_branch_name # The first element in the tuples in 'rebases' is the rebase id, which # is an ever-increasing serial number that we can use as an indication # of the order in which the rebases were made. rebases = [Rebase(*rebase) for rebase in sorted(rebases)] rebase_old_heads = set(rebase.old_head for rebase in rebases) heads -= rebase_old_heads assert 0 <= len(heads) <= 1 if not heads: heads = set([rebases[-1].new_head]) if repository: target.addInternalScript(repository.getJS()) processed = set() summaries = {} for commit in commits: summary = commit.summary().strip() summaries[summary] = commit summaries[commit.sha1] = commit if extra_commits: for commit in extra_commits: summary = commit.summary().strip() summaries[summary] = commit summaries[commit.sha1] = commit def isFixupOrSquash(commit): key, _, summary = commit.summary().partition(" ") if key in ("fixup!", "squash!"): what = key[:-1] else: return None summary = summary.strip() commit = summaries.get(summary) if not commit and re.match("[0-9A-Fa-f]{40}$", summary): commit = summaries.get(summary) if not commit: try: sha1 = repository.revparse(summary) commit = Commit.fromSHA1(db, repository, sha1) except Exception: pass if commit: summary = commit.summary() return what, summary for width, column in columns: if isinstance(column, SummaryColumn): column.isFixupOrSquash = isFixupOrSquash break def output(table, commit, overrides={}): if commit not in processed: classes = ["commit"] row_id = None if len(commit.parents) > 1: classes.append("merge") if highlight == commit: classes.append("highlight") row_id = commit.sha1 row = table.tr(" ".join(classes), id=row_id) profiler.check("log: rendering: row") for index, (width, column) in enumerate(columns): column.render(db, commit, row.td(column.className(db, commit)), overrides=overrides) profiler.check("log: rendering: column %d" % (index + 1)) processed.add(commit) return row else: return None cursor = db.cursor() def emptyCommit(commit): cursor.execute("""SELECT 1 FROM fileversions JOIN changesets ON (changesets.id=fileversions.changeset) JOIN reviewchangesets ON (reviewchangesets.changeset=changesets.id) WHERE changesets.child=%s AND reviewchangesets.review=%s""", (commit.getId(db), review.id)) return not cursor.fetchone() def inner(target, head, tails, align='right', title=None, table=None, silent_if_empty=set(), upstream=None): if not table: table = target.table('log', align=align, cellspacing=0) for width, column in columns: table.col(width=('%d%%' % width)) if title: thead = table.thead() row = thead.tr("title") header = row.td("h1", colspan=len(columns)).h1() header.text(title) if callable(title_right): title_right(db, header.span("right")) row = thead.tr('headings') for width, column in columns: column.heading(row.td(column.className(db, None))) elif head is None or head in tails: if upstream: tag = upstream.findInterestingTag(db) if tag: what = tag else: what = upstream.sha1[:8] message = "Merged with base branch (%s)." % what else: message = "Merged with base branch." thead = table.thead() row = thead.tr('basemerge') row.td(colspan=len(columns), align='center').text(message) return (None, None, None, False) tbody = table.tbody() commit = head last_commit = None skipped = True while commit and commit not in tails: suppress = False optional_merge = False listed = listed_commits is None or commit.getId(db) in listed_commits if commit in silent_if_empty and emptyCommit(commit): # This is a clean automatically generated merge commit; pretend it isn't here at all. suppress = True if not suppress and not listed: suppress = len(commit.parents) == 1 optional_merge = not suppress if not suppress: commit_tr = output(tbody, commit) else: commit_tr = None if listed: skipped = False last_commit = commit if len(commit.parents) == 0: break elif len(commit.parents) == 1: commit = commit_set.get(commit.parents[0]) elif len(commit.parents) > 1: if len(commit.parents) > 2: common_ancestors = commit_set.getCommonAncestors(commit) match = re_octopus.match(commit.message.split("\n", 1)[0]) if match: titles = re.findall("'([^']+)'", match.group(1)) if len(titles) != len(commit.parents): titles = None else: titles = None for index, sha1 in enumerate(commit.parents): if sha1 in commit_set: sublog = tbody.tr('sublog') inner_last_commit, inner_table, inner_tail, inner_skipped = inner(sublog.td(colspan=len(columns)), commit_set[sha1], common_ancestors, title=titles and titles[index] or None) if inner_skipped: sublog.remove() if optional_merge and commit_tr: commit_tr.remove() if not common_ancestors: return (None, None, None, False) commit = common_ancestors.pop() continue parent1_sha1 = commit.parents[0] parent2_sha1 = commit.parents[1] parent1 = commit_set.get(parent1_sha1) parent2 = commit_set.get(parent2_sha1) # TODO: Try to remember what this code actually does, and why... if parent1_sha1 in rebase_old_heads: if parent2: sublog = tbody.tr('sublog') inner_last_commit, inner_table, inner_tail, inner_skipped = \ inner(sublog.td(colspan=len(columns)), parent2, tails) if inner_skipped: sublog.remove() if optional_merge and commit_tr: commit_tr.remove() return (commit, table, parent1_sha1, False) elif parent2_sha1 in rebase_old_heads: if parent1: sublog = tbody.tr('sublog') inner_last_commit, inner_table, inner_tail, inner_skipped = \ inner(sublog.td(colspan=len(columns)), parent1, tails) if inner_skipped: sublog.remove() if optional_merge and commit_tr: commit_tr.remove() return (commit, table, parent2_sha1, False) if parent1 and parent2: common_ancestors = commit_set.getCommonAncestors(commit) merged_remote_into_local = re_remote_into_local.match(commit.summary()) or re_side_into_main.match(commit.summary()) def rankPaths(commit, tails): shortest = None shortest_length = len(commit_set) longest = None longest_length = 0 for sha1 in commit.parents: parent = commit_set[sha1] counted = set() pending = set([parent]) while pending: candidate = pending.pop() if candidate in counted: continue if candidate in tails: continue counted.add(candidate) pending.update(commit_set.getParents(candidate)) length = len(counted) if length < shortest_length: shortest = parent shortest_length = length if length >= longest_length: longest = parent longest_length = length return shortest, shortest_length, longest, longest_length show_merged, shortest_length, show_normal, longest_length = rankPaths(commit, common_ancestors | tails) display_parallel = False if merged_remote_into_local and shortest_length * 2 > longest_length: if len(common_ancestors) == 1 and len(commit_set.filtered([commit]).getTails()) == 1: display_parallel = True else: show_merged = parent2 show_normal = parent1 if display_parallel: all_empty = True for sha1 in commit.parents: sublog = tbody.tr('sublog') inner_last_commit, inner_table, inner_tail, inner_skipped = inner(sublog.td(colspan=len(columns)), commit_set[sha1], common_ancestors | tails) if inner_skipped: sublog.remove() else: all_empty = False if all_empty and optional_merge and commit_tr: commit_tr.remove() commit = common_ancestors.pop() else: sublog = tbody.tr('sublog') inner_last_commit, inner_table, inner_tail, inner_skipped = inner(sublog.td(colspan=len(columns)), show_merged, common_ancestors | tails) if inner_skipped: sublog.remove() if optional_merge and commit_tr: commit_tr.remove() commit = show_normal else: if parent1: upstream_sha1 = parent2_sha1 else: upstream_sha1 = parent1_sha1 if not commit in silent_if_empty: # Merge with the base branch. inner(tbody.tr('sublog').td(colspan=len(columns)), None, None, upstream=Commit.fromSHA1(db, repository, upstream_sha1)) if parent1: commit = parent1 else: commit = parent2 return (last_commit, table, last_commit.parents[0] if last_commit and last_commit.parents else None, skipped) class_name = "paleyellow log" if collapsable: class_name += " collapsable" table = target.table(class_name, align='center', cellspacing=0) for width, column in columns: table.col(width=('%d%%' % width)) thead = table.thead("title") row = thead.tr("title") header = row.td("h1", colspan=len(columns)).h1() error_message = None if len(commit_set) == 0: thead = table.thead() row = thead.tr('error') cell = row.td(colspan=len(columns), align='center') cell.text("No commits. ") if review: review.branch.loadCommits(db) cell.a(href="showtree?sha1=%s&review=%d" % (review.branch.head.sha1, review.id)).text("[Browse tree]") return elif len(heads) > 1: error_message = "Invalid commit set: Multiple heads." elif len(heads) == 0: error_message = "Invalid commit set: No heads." if error_message is not None: thead = table.thead() row = thead.tr('error') cell = row.td(colspan=len(columns), align='center') cell.text(error_message) return head = heads.pop() if heads else None row = thead.tr('headings') for width, column in columns: column.heading(row.td(column.className(db, None))) first_rebase = True silent_if_empty = set() if rebases: for rebase in rebases: if rebase.new_upstream or rebase.target_branch_name: silent_if_empty.add(rebase.old_head) top_rebases = [] while head == rebases[-1].new_head: rebase = rebases.pop() top_rebases.append((head, rebase)) head = rebase.old_head if head in commit_set: break for rebase_head, rebase in top_rebases: thead = table.thead("rebase") row = thead.tr('rebase') cell = row.td(colspan=len(columns), align='center') if rebase.new_upstream is None and not rebase.target_branch_name: cell.text("History rewritten") else: cell.text("Branch rebased onto ") if rebase.target_branch_name: anchor = cell.a(href=("/checkbranch?repository=%d&commit=%s" % (repository.id, rebase.target_branch_name))) anchor.text(rebase.target_branch_name) else: upstream_description = repository.describe(db, rebase.new_upstream.sha1) if upstream_description is None: upstream_description = rebase.new_upstream.sha1[:8] anchor = cell.a(href="/%s/%s" % (repository.name, rebase.new_upstream.sha1)) anchor.text(upstream_description) cell.text(" by %s" % rebase.user.fullname) if first_rebase: cell.text(": ") review_param = "&review=%d" % review.id if review else "" cell.a(href="log?repository=%d&branch=%s%s" % (repository.id, branch_name, review_param)).text("[actual log]") if user and user == rebase.user: cell.text(" ") cell.a(href="javascript:revertRebase(%d)" % rebase.id).text("[revert]") first_rebase = False else: cell.text(".") if rebase.new_head in conflicts and not emptyCommit(rebase.new_head): output(table, rebase.new_head, overrides={ "type": "Rebase", "summary": "Changes introduced by rebase", "summary_classnames": ["rebase"], "author": rebase.user, "rebase_conflicts": conflicts[rebase.new_head] }) while True: # 'local_tails' is the set of commits that, when reached, should make # inner() stop outputting commits and instead return. This set of # commits contains all the "tails" of the whole commit-set we're # rendering (in the 'tails' set here), as well as the "new head" of the # next rebase to be output. local_tails = tails.copy() if rebases: local_tails.add(rebases[-1].new_head) last_commit, table, tail, skipped = inner( target, head, local_tails, 'center', title, table, silent_if_empty) if rebases: rebase = rebases.pop() assert tail == rebase.new_head, "tail (%s) != rebase.new_head (%s)" % (tail, rebase.new_head) while True: head = rebase.old_head thead = table.thead("rebase") row = thead.tr('rebase') cell = row.td(colspan=len(columns), align='center') if rebase.new_upstream is None and not rebase.target_branch_name: cell.text("History rewritten") else: cell.text("Branch rebased onto ") if rebase.target_branch_name: anchor = cell.a(href=("/checkbranch?repository=%d&commit=%s" % (repository.id, rebase.target_branch_name))) anchor.text(rebase.target_branch_name) else: upstream_description = repository.describe(db, rebase.new_upstream.sha1) if upstream_description is None: upstream_description = rebase.new_upstream.sha1[:8] anchor = cell.a(href="/%s/%s" % (repository.name, rebase.new_upstream.sha1)) anchor.text(upstream_description) cell.text(" by %s" % rebase.user.fullname) if first_rebase: cell.text(": ") review_param = "&review=%d" % review.id if review else "" cell.a(href="log?repository=%d&branch=%s%s" % (repository.id, branch_name, review_param)).text("[actual log]") first_rebase = False else: cell.text(".") if rebase.new_head in conflicts: if len(rebase.new_head.parents) < 2 and not emptyCommit(rebase.new_head): output(table, rebase.new_head, overrides={ "type": "Rebase", "summary": "Changes introduced by rebase", "summary_classnames": ["rebase"], "author": rebase.user, "rebase_conflicts": conflicts[rebase.new_head] }) if rebases and rebases[-1].new_head == head: rebase = rebases.pop() else: break continue if last_commit: if len(last_commit.parents) == 1: upstream = Commit.fromSHA1(db, repository, last_commit.parents[0]) upstream_description = repository.describe(db, upstream.sha1) if not upstream_description: upstream_description = upstream.sha1[:8] row = table.thead("rebase").tr('upstream') cell = row.td(colspan=len(columns), align='center') cell.text("Based on: ") anchor = cell.a(href="/%s/%s" % (repository.name, upstream.sha1)) anchor.text(upstream_description) if callable(bottom_right): bottom_right(db, table.tfoot().tr().td(colspan=len(columns))) break profiler.check("log: rendering") if "%d" in title: header.text(title % len(processed)) else: header.text(title) if callable(title_right): title_right(db, header.span("right"))
def render(db, target, title, branch=None, commits=None, columns=DEFAULT_COLUMNS, title_right=None, listed_commits=None, rebases=None, branch_name=None, bottom_right=None, review=None, highlight=None, profiler=None, collapsable=False, user=None, extra_commits=None): addResources(target) if not profiler: profiler = Profiler() profiler.check("log: start") if branch is not None: repository = branch.repository branch.loadCommits(db) commits = branch.commits[:] commit_set = log.commitset.CommitSet(branch.commits) else: assert commits is not None repository = commits[0].repository if len(commits) else None commit_set = log.commitset.CommitSet(commits) profiler.check("log: commits") heads = commit_set.getHeads() tails = commit_set.getTails() if rebases is not None: frebases = dict([(old_head, (rebase_id, new_head, rebase_user, new_upstream, target_branch_name)) for rebase_id, old_head, new_head, rebase_user, new_upstream, target_branch_name in rebases]) rrebases = dict([(new_head, (rebase_id, old_head, rebase_user, new_upstream, target_branch_name)) for rebase_id, old_head, new_head, rebase_user, new_upstream, target_branch_name in rebases]) for rebase_id, old_head, new_head, rebase_user, new_upstream, target_branch_name in rebases: if old_head in heads: next_head = new_head while next_head: for head in heads: if head == old_head: continue elif commit_set.isAncestorOf( next_head, head) or next_head in tails: heads.remove(old_head) next_head = None break else: next_head = frebases.get(next_head, (None, None))[1] else: frebases = None rrebases = None if repository: target.addInternalScript(repository.getJS()) processed = set() summaries = {} for commit in commits: summary = commit.summary().strip() summaries[summary] = commit if len(summary) > 60: summaries[summary[:40]] = commit summaries[commit.sha1] = commit summaries[commit.sha1[:8]] = commit if extra_commits: for commit in extra_commits: summary = commit.summary().strip() summaries[summary] = commit if len(summary) > 60: summaries[summary[:40]] = commit summaries[commit.sha1] = commit summaries[commit.sha1[:8]] = commit def isFixupOrSquash(commit): if commit.message.startswith("fixup! "): what = "fixup" summary = commit.summary()[6:].strip() elif commit.message.startswith("squash! "): what = "squash" summary = commit.summary()[7:].strip() else: return None commit = (summaries.get(summary) or summaries.get(summary[:40]) or summaries.get(summary[:8])) if commit: return what, commit else: return None for width, column in columns: if isinstance(column, SummaryColumn): column.isFixupOrSquash = isFixupOrSquash break def output(table, commit): if commit not in processed: classes = ["commit"] row_id = None if len(commit.parents) > 1: classes.append("merge") if highlight == commit: classes.append("highlight") row_id = commit.sha1 row = table.tr(" ".join(classes), id=row_id) profiler.check("log: rendering: row") for index, (width, column) in enumerate(columns): column.render(db, commit, row.td(column.className(db, commit))) profiler.check("log: rendering: column %d" % (index + 1)) processed.add(commit) return row else: return None cursor = db.cursor() def inner(target, head, tails, align='right', title=None, table=None, silent_merges=set(), upstream=None): if not table: table = target.table('log', align=align, cellspacing=0) for width, column in columns: table.col(width=('%d%%' % width)) if title: thead = table.thead() row = thead.tr("title") header = row.td("h1", colspan=len(columns)).h1() header.text(title) if callable(title_right): title_right(db, header.span("right")) row = thead.tr('headings') for width, column in columns: column.heading(row.td(column.className(db, None))) elif head is None or head in tails: if upstream: tag = upstream.findInterestingTag(db) if tag: what = tag else: what = upstream.sha1[:8] message = "Merged with base branch (%s)." % what else: message = "Merged with base branch." thead = table.thead() row = thead.tr('basemerge') row.td(colspan=len(columns), align='center').text(message) return (None, None, None, False) tbody = table.tbody() commit = head sha1s = [] last_commit = None skipped = True while commit and commit not in tails: suppress = False optional_merge = False listed = listed_commits is None or commit.getId( db) in listed_commits if len(commit.parents) > 1 and commit in silent_merges: cursor.execute( """SELECT 1 FROM fileversions JOIN changesets ON (changesets.id=fileversions.changeset) JOIN reviewchangesets ON (reviewchangesets.changeset=changesets.id) WHERE changesets.child=%s AND reviewchangesets.review=%s""", (commit.getId(db), review.id)) if not cursor.fetchone(): # This is a clean automatically generated merge commit; pretend it isn't here at all. suppress = True if not suppress and not listed: suppress = len(commit.parents) == 1 optional_merge = not suppress if not suppress: commit_tr = output(tbody, commit) else: commit_tr = None if listed: skipped = False last_commit = commit if len(commit.parents) == 0: break elif len(commit.parents) == 1: commit = commit_set.get(commit.parents[0]) elif len(commit.parents) > 1: if len(commit.parents) > 2: common_ancestors = commit_set.getCommonAncestors(commit) match = re_octopus.match(commit.message.split("\n", 1)[0]) if match: titles = re.findall("'([^']+)'", match.group(1)) if len(titles) != len(commit.parents): titles = None else: titles = None for index, sha1 in enumerate(commit.parents): if sha1 in commit_set: sublog = tbody.tr('sublog') inner_last_commit, inner_table, inner_tail, inner_skipped = inner( sublog.td(colspan=len(columns)), commit_set[sha1], common_ancestors, title=titles and titles[index] or None) if inner_skipped: sublog.remove() if optional_merge and commit_tr: commit_tr.remove() if not common_ancestors: return (None, None, None, False) commit = common_ancestors.pop() continue parent1_sha1 = commit.parents[0] parent2_sha1 = commit.parents[1] parent1 = commit_set.get(parent1_sha1) parent2 = commit_set.get(parent2_sha1) if rrebases: if parent1_sha1 in rrebases: if parent2: sublog = tbody.tr('sublog') inner_last_commit, inner_table, inner_tail, inner_skipped = inner( sublog.td(colspan=len(columns)), parent2, tails) if inner_skipped: sublog.remove() if optional_merge and commit_tr: commit_tr.remove() return (commit, table, parent1_sha1, False) elif parent2_sha1 in rrebases: if parent1: sublog = tbody.tr('sublog') inner_last_commit, inner_table, inner_tail, inner_skipped = inner( sublog.td(colspan=len(columns)), parent1, tails) if inner_skipped: sublog.remove() if optional_merge and commit_tr: commit_tr.remove() return (commit, table, parent2_sha1, False) #raise Exception, repr([parent1_sha1, parent1, parent2_sha1, parent2]) if parent1 and parent2: common_ancestors = commit_set.getCommonAncestors(commit) merged_remote_into_local = re_remote_into_local.match( commit.summary()) or re_side_into_main.match( commit.summary()) def rankPaths(commit, tails): shortest = None shortest_length = len(commit_set) longest = None longest_length = 0 for sha1 in commit.parents: parent = commit_set[sha1] counted = set() pending = set([parent]) while pending: candidate = pending.pop() if candidate in counted: continue if candidate in tails: continue counted.add(candidate) pending.update( commit_set.getParents(candidate)) length = len(counted) if length < shortest_length: shortest = parent shortest_length = length if length >= longest_length: longest = parent longest_length = length return shortest, shortest_length, longest, longest_length show_merged, shortest_length, show_normal, longest_length = rankPaths( commit, common_ancestors | tails) display_parallel = False if merged_remote_into_local and shortest_length * 2 > longest_length: if len(common_ancestors) == 1 and len( commit_set.filtered([commit]).getTails()) == 1: display_parallel = True else: show_merged = parent2 show_normal = parent1 if display_parallel: all_empty = True for sha1 in commit.parents: sublog = tbody.tr('sublog') inner_last_commit, inner_table, inner_tail, inner_skipped = inner( sublog.td(colspan=len(columns)), commit_set[sha1], common_ancestors | tails) if inner_skipped: sublog.remove() else: all_empty = False if all_empty and optional_merge and commit_tr: commit_tr.remove() commit = common_ancestors.pop() else: sublog = tbody.tr('sublog') inner_last_commit, inner_table, inner_tail, inner_skipped = inner( sublog.td(colspan=len(columns)), show_merged, common_ancestors | tails) if inner_skipped: sublog.remove() if optional_merge and commit_tr: commit_tr.remove() commit = show_normal else: if parent1: upstream_sha1 = parent2_sha1 else: upstream_sha1 = parent1_sha1 if not commit in silent_merges: # Merge with the base branch. inner(tbody.tr('sublog').td(colspan=len(columns)), None, None, upstream=Commit.fromSHA1(db, repository, upstream_sha1)) if parent1: commit = parent1 else: commit = parent2 return (last_commit, table, last_commit.parents[0] if last_commit and last_commit.parents else None, skipped) class_name = "paleyellow log" if collapsable: class_name += " collapsable" table = target.table(class_name, align='center', cellspacing=0) for width, column in columns: table.col(width=('%d%%' % width)) thead = table.thead("title") row = thead.tr("title") header = row.td("h1", colspan=len(columns)).h1() error_message = None if len(commit_set) == 0: thead = table.thead() row = thead.tr('error') cell = row.td(colspan=len(columns), align='center') cell.text("No commits. ") if review: cell.a(href="showtree?sha1=%s&review=%d" % (review.branch.head.sha1, review.id)).text("[Browse tree]") return elif len(heads) > 1: error_message = "Invalid commit set: Multiple heads." elif len(heads) == 0: error_message = "Invalid commit set: No heads." if error_message is not None: thead = table.thead() row = thead.tr('error') cell = row.td(colspan=len(columns), align='center') cell.text(error_message) return head = heads.pop() if heads else None row = thead.tr('headings') for width, column in columns: column.heading(row.td(column.className(db, None))) first_rebase = True silent_merges = set() if frebases: rebase_head = head top_rebases = [] while rebase_head in frebases: top_rebases.insert(0, (rebase_head, frebases[rebase_head])) rebase_head = frebases[rebase_head][1] del rrebases[rebase_head] for rebase_head, rebase in top_rebases: thead = table.thead("rebase") row = thead.tr('rebase') cell = row.td(colspan=len(columns), align='center') new_upstream = rebase[3] if new_upstream is None and not rebase[4]: cell.text("History rewritten") else: cell.text("Branch rebased onto ") if rebase[4]: cell.a(href="/checkbranch?repository=%d&commit=%s" % (repository.id, rebase[4])).text(rebase[4]) else: tag = new_upstream.findInterestingTag(db) if tag: cell.text(tag) else: cell.a(href="/%s/%s" % (repository.name, new_upstream.sha1)).text( new_upstream.sha1[:8]) silent_merges.add(rebase_head) cell.text(" by %s" % rebase[2].fullname) if first_rebase: cell.text(": ") cell.a(href="log?repository=%d&branch=%s" % (repository.id, branch_name)).text("[actual log]") if rebase[0] is not None and user == rebase[2]: cell.text(" ") cell.a(href="javascript:revertRebase(%d)" % rebase[0]).text("[revert]") first_rebase = False else: cell.text(".") while True: last_commit, table, tail, skipped = inner(target, head, tails, 'center', title, table, silent_merges) if rrebases and last_commit: try: rebase = rrebases.pop(tail) except: break while True: head = rebase[1] thead = table.thead("rebase") row = thead.tr('rebase') cell = row.td(colspan=len(columns), align='center') new_upstream = rebase[3] if new_upstream is None and not rebase[4]: cell.text("History rewritten") else: cell.text("Branch rebased onto ") if rebase[4]: cell.a(href="/checkbranch?repository=%d&commit=%s" % (repository.id, rebase[4])).text(rebase[4]) else: tag = new_upstream.findInterestingTag(db) if tag: cell.text(tag) else: cell.a(href="/%s/%s" % (repository.name, new_upstream.sha1)).text( new_upstream.sha1[:8]) silent_merges.add(head) cell.text(" by %s" % rebase[2].fullname) if first_rebase: cell.text(": ") cell.a(href="log?repository=%d&branch=%s" % (repository.id, branch_name)).text("[actual log]") first_rebase = False else: cell.text(".") if head in rrebases: rebase = rrebases.pop(head) else: break continue if last_commit: if len(last_commit.parents) == 1: tag = repository.findInterestingTag(db, last_commit.parents[0]) if tag: thead = table.thead("rebase") row = thead.tr('upstream') cell = row.td(colspan=len(columns), align='center') cell.text("Based on: %s" % tag) if callable(bottom_right): bottom_right(db, table.tfoot().tr().td(colspan=len(columns))) break profiler.check("log: rendering") if "%d" in title: header.text(title % len(processed)) else: header.text(title) if callable(title_right): title_right(db, header.span("right"))