summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xgit-bz151
1 files changed, 143 insertions, 8 deletions
diff --git a/git-bz b/git-bz
index 7950ff5..84fd7be 100755
--- a/git-bz
+++ b/git-bz
@@ -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)