diff options
-rwxr-xr-x | git-bz | 151 |
1 files changed, 143 insertions, 8 deletions
@@ -91,6 +91,13 @@ # If the argument identifies a commit or commits rather than a bug # then each bug referred to in the commits is edited in turn. # +# If -p/--pushed is specified, then git-bz will attempt to automatically +# determine the correct comments, attachment changes, and resolution +# for the bug from applying the specified commits to the project's +# official repository. You'll have a chance to edit these changes and +# add additional comments. See 'git bz push' for a convenient interface +# to push commits and do this at the same time. +# # git bz file [options] [[<product>]/<component>] [<commit> | <revision range>] # # Like 'attach', but files a new bug. Opens an editor for the user to @@ -111,6 +118,16 @@ # # on a different bug tracker # git bz -b bugs.freedesktop.org file my-product/some-component b50ea9bd^.. # +# git bz push [options] [<repository> <refspec>...] +# +# Exactly like 'git push', but 'git bz edit --pushed' is done for each +# bug referenced in the newly pushed commits. +# +# Note that "newly pushed commits" are commits that were added to any +# existing branch by the push. Commits don't have to be pushed to master +# to be considered newly pushed. However, commits pushed to on newly +# created branches will be ignored. +# # Authentication # ============== # In order to use git-bz you need to already be logged into the bug tracker @@ -1369,7 +1386,42 @@ def do_attach(bug_reference, commit_or_revision_range): attach_commits(bug, commits, edit_comments=global_options.edit) -def edit_bug(bug): +# Sort the patches in the bug into categories based on a set of Git +# git commits that we're considering to be newly applied. Matching +# is done on exact git subject <=> patch description matches. +def filter_patches(bug, applied_commits): + newly_applied_patches = dict() # maps to the commit object where it was applied + obsoleted_patches = set() + unapplied_patches = set() + + applied_subjects = dict(((commit.subject, commit) for commit in applied_commits)) + seen_subjects = set() + + # Work backwards so that the latest patch is considered applied, and older + # patches with the same subject obsoleted. + for patch in reversed(bug.patches): + # Previously committted or rejected patches are never a match + if patch.status == "committed" or patch.status == "rejected": + continue + + if patch.description in seen_subjects: + obsoleted_patches.add(patch) + elif patch.description in applied_subjects: + newly_applied_patches[patch] = applied_subjects[patch.description] + seen_subjects.add(patch) + else: + unapplied_patches.append(patch) + + return newly_applied_patches, obsoleted_patches, unapplied_patches + +def edit_bug(bug, applied_commits=None): + if applied_commits is not None: + newly_applied_patches, obsoleted_patches, unapplied_patches = filter_patches(bug, applied_commits) + mark_resolved = len(unapplied_patches) == 0 and bug.bug_status != "RESOLVED" + else: + newly_applied_patches = obsoleted_patches = set() + mark_resolved = False + template = StringIO() template.write("# Bug %d - %s - %s" % (bug.id, bug.short_desc, bug.bug_status)) if bug.bug_status == "RESOLVED": @@ -1377,22 +1429,44 @@ def edit_bug(bug): template.write("\n") template.write("# %s\n" % bug.get_url()) template.write("# Enter comment on following lines; delete everything to abort\n\n") - template.write("# Uncomment to resolve bug\n") + + for patch in bug.patches: + if patch in newly_applied_patches: + commit = newly_applied_patches[patch] + template.write("Attachment %d pushed as %s - %s\n" % (patch.attach_id, commit.id[0:7], commit.subject)) + + if mark_resolved: + template.write("# Comment to keep bug open\n") + elif bug.bug_status == "RESOLVED": + template.write("# Uncommment and edit to change resolution\n") + else: + template.write("# Uncomment to resolve bug\n") legal_resolutions = bug.legal_values('resolution') if legal_resolutions: # Require non-empty resolution. DUPLICATE, MOVED would need special support legal_resolutions = [x for x in legal_resolutions if x not in ('', 'DUPLICATE', 'MOVED')] template.write("# possible resolutions: %s\n" % abbreviation_help_string(legal_resolutions)) - template.write("#Resolution: FIXED\n") + if not mark_resolved: + template.write("#") + template.write("Resolution: FIXED\n") if len(bug.patches) > 0: - template.write("\n# To change patch status, uncomment below, edit 'committed' as appropriate.\n") + if len(newly_applied_patches) > 0 or len(obsoleted_patches) > 0: + template.write("\n# Lines below change patch status, unless commented out\n") + else: + template.write("\n# To change patch status, uncomment below, edit 'committed' as appropriate.\n") legal_statuses = bug.legal_values('attachments.status') if legal_statuses: legal_statuses.append('obsolete') template.write("# possible statuses: %s\n" % abbreviation_help_string(legal_statuses)) for patch in bug.patches: - template.write("#committed @%d - %s - %s\n" % (patch.attach_id, patch.description, patch.status)) + if patch in newly_applied_patches: + new_status = "committed" + elif patch in obsoleted_patches: + new_status = "obsolete" + else: + new_status = "#committed" + template.write("%s @%d - %s - %s\n" % (new_status, patch.attach_id, patch.description, patch.status)) template.write("\n") lines = edit_template(template.getvalue()) @@ -1528,7 +1602,10 @@ def extract_and_collate_bugs(commits): def do_edit(bug_reference_or_revision_range): try: - bug = Bug.load(BugHandle.parse(bug_reference_or_revision_range)) + handle = BugHandle.parse(bug_reference_or_revision_range) + if global_options.pushed: + die("-p/--pushed can't be used together with a bug reference") + bug = Bug.load(handle) edit_bug(bug) except BugParseError, e: try: @@ -1539,7 +1616,10 @@ def do_edit(bug_reference_or_revision_range): commits.reverse() for handle, commits in extract_and_collate_bugs(commits): bug = Bug.load(handle) - edit_bug(bug) + if global_options.pushed: + edit_bug(bug, commits) + else: + edit_bug(bug) PRODUCT_COMPONENT_HELP = """ @@ -1626,6 +1706,49 @@ def do_file(*args): attach_commits(bug, commits, include_comments=include_comments) +def do_push(*args): + # Predicting what 'git pushes' pushes based on the command line + # would be extraordinarily complex, but the interactive output goes + # to stderr and is somewhat ambiguous. We do the best we can parsing + # it. git 1.6.4 adds --porcelain to push, so we can use that eventually. + try: + if global_options.force: + out, err = git.push(*args, force=True, _return_stderr=True) + else: + out, err = git.push(*args, _return_stderr=True) + except CalledProcessError: + return + # Echo the output so the user gets feedback about what happened + print >>sys.stderr, err + + commits = [] + for line in err.strip().split("\n"): + # + # We only look for updates of existing branches; a much more complex + # handling would be look for all commits that weren't pushed to a + # remote branch. Hopefully the typical use of 'git bz push' is pushing + # a single commit to master. + # + # e5ad33e..febe0d4 master -> master + m = re.match(r"^\s*([a-f0-9]{6,}..[a-f0-9]{6,})\s+\S+\s*->\s*\S+\s*$", line) + if m: + branch_commits = get_commits(m.group(1)) + # Process from oldest to newest + branch_commits.reverse() + commits += branch_commits + + # Remove duplicate commits + seen_commit_ids = set() + unique_commits = [] + for commit in commits: + if not commit.id in seen_commit_ids: + seen_commit_ids.add(commit.id) + unique_commits.append(commit) + + for handle, commits in extract_and_collate_bugs(commits): + bug = Bug.load(handle) + edit_bug(bug, commits) + ################################################################################ if len(sys.argv) > 1: @@ -1660,13 +1783,21 @@ elif command == 'attach': add_edit_option() min_args = max_args = 2 elif command == 'edit': - parser.set_usage("git bz edit [options] <bug reference>"); + parser.set_usage("git bz edit [options] [<bug reference> | <commit> | <revision range>]"); + parser.add_option("-p", "--pushed", action="store_true", + help="pre-fill edit form treating the commits as pushed") min_args = max_args = 1 elif command == 'file': parser.set_usage("git bz file [options] <product>/<component> [<since> | <revision range>]"); add_add_url_option() min_args = 1 max_args = 2 +elif command == 'push': + parser.set_usage("git bz push [options] [<repository> <refspec>...]"); + parser.add_option("-f", "--force", action="store_true", + help="allow non-fast-forward commits") + min_args = 0 + max_args = 1000 # no max else: print >>sys.stderr, "Usage: git bz [add-url|apply|attach|edit|file] [options]" sys.exit(1) @@ -1684,8 +1815,12 @@ elif command == 'apply': elif command == 'attach': do_attach(*args) elif command == 'edit': + if global_options.pushed: + exit do_edit(*args) elif command == 'file': do_file(*args) +elif command == 'push': + do_push(*args) sys.exit(0) |