summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorOwen W. Taylor <otaylor@fishsoup.net>2009-08-30 21:06:15 -0400
committerOwen W. Taylor <otaylor@fishsoup.net>2009-08-30 21:06:15 -0400
commitc1fdc4c75da29583c33f213ebe970e55d2bcfbe8 (patch)
tree18cd8af4289d7f8961568055c24fb20e304d8994
parentaaa94d05aabd81bee0874f4b596b71fded3033ad (diff)
downloadgit-bz-c1fdc4c75da29583c33f213ebe970e55d2bcfbe8.tar.gz
Add 'git bz edit --pushed' and 'git bz push'
'git bz edit --pushed <commits>' automatically prepares the bug editing buffering based on a guess as what edits (comments, resolving the bug, changing attachment status) are appropriate once the commits are pushed to the project's official repository. 'git bz push' actually pushes commits and then does 'git bz edit --pushed'
-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)