<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[47343] trunk/WebKitTools/Scripts</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://trac.webkit.org/projects/webkit/changeset/47343">47343</a></dd>
<dt>Author</dt> <dd>ddkilzer@apple.com</dd>
<dt>Date</dt> <dd>2009-08-16 17:30:53 -0700 (Sun, 16 Aug 2009)</dd>
</dl>

<h3>Log Message</h3>
<pre>WIP: bugzilla-tool: add land-commits command</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkWebKitToolsScriptsbugzillatool">trunk/WebKitTools/Scripts/bugzilla-tool</a></li>
<li><a href="#trunkWebKitToolsScriptsmodulesscmpy">trunk/WebKitTools/Scripts/modules/scm.py</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunkWebKitToolsScriptsbugzillatool"></a>
<div class="modfile"><h4>Modified: trunk/WebKitTools/Scripts/bugzilla-tool (47342 => 47343)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/WebKitTools/Scripts/bugzilla-tool        2009-08-16 23:24:44 UTC (rev 47342)
+++ trunk/WebKitTools/Scripts/bugzilla-tool        2009-08-17 00:30:53 UTC (rev 47343)
</span><span class="lines">@@ -56,6 +56,8 @@
</span><span class="cx">         noun = plural(noun)
</span><span class="cx">     return &quot;%d %s&quot; % (count, noun)
</span><span class="cx"> 
</span><ins>+REVIEWER_PLACEHOLDER = &quot;NOBODY (OOPS!)&quot;
+
</ins><span class="cx"> def view_source_url(revision_number):
</span><span class="cx">      return &quot;http://trac.webkit.org/changeset/%s&quot; % revision_number
</span><span class="cx"> 
</span><span class="lines">@@ -89,7 +91,7 @@
</span><span class="cx"> def set_reviewer_in_changelog(changelog_path, reviewer):
</span><span class="cx">     # inplace=1 creates a backup file and re-directs stdout to the file
</span><span class="cx">     for line in fileinput.FileInput(changelog_path, inplace=1):
</span><del>-        print line.replace(&quot;NOBODY (OOPS!)&quot;, reviewer.encode(&quot;utf-8&quot;)), # Trailing comma suppresses printing newline
</del><ins>+        print line.replace(REVIEWER_PLACEHOLDER, reviewer.encode(&quot;utf-8&quot;)), # Trailing comma suppresses printing newline
</ins><span class="cx"> 
</span><span class="cx"> def modified_changelogs(scm):
</span><span class="cx">     changelog_paths = []
</span><span class="lines">@@ -126,7 +128,25 @@
</span><span class="cx">     # FIXME: We should sort and label the ChangeLog messages like commit-log-editor does.
</span><span class="cx">     return CommitMessage(''.join(changelog_messages).splitlines())
</span><span class="cx"> 
</span><ins>+def guess_reviewer_from_bug(bugs, bug_id):
+    patches = bugs.fetch_reviewed_patches_from_bug(bug_id)
+    if len(patches) != 1:
+        log(&quot;%s reviewed patches on bug %s, cannot infer reviewer.&quot; % (&quot;Too many&quot; if len(patches) &gt; 1 else &quot;No&quot;, bug_id))
+        return None
+    patch = patches[0]
+    reviewer = patch['reviewer']
+    log('Guessing &quot;%s&quot; as reviewer from attachment %s on bug %s.' % (reviewer, patch['id'], bug_id))
+    return reviewer
</ins><span class="cx"> 
</span><ins>+def update_changelogs_with_reviewer(reviewer, bug_id, tool):
+    if not reviewer:
+        error(&quot;No reviewer available; can't update ChangeLogs.&quot;);
+
+    changelogs = modified_changelogs(tool.scm())
+    for changelog_path in changelogs:
+        set_reviewer_in_changelog(changelog_path, reviewer)
+
+
</ins><span class="cx"> class Command:
</span><span class="cx">     def __init__(self, help_text, argument_names=&quot;&quot;, options=[], requires_local_commits=False):
</span><span class="cx">         self.help_text = help_text
</span><span class="lines">@@ -216,6 +236,8 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> def bug_comment_from_commit_text(scm, commit_text):
</span><ins>+    if scm.dryrun:
+        return commit_text
</ins><span class="cx">     match = re.search(scm.commit_success_regexp(), commit_text, re.MULTILINE)
</span><span class="cx">     svn_revision = match.group('svn_revision')
</span><span class="cx">     commit_text += (&quot;\n%s&quot; % view_source_url(svn_revision))
</span><span class="lines">@@ -287,54 +309,75 @@
</span><span class="cx">             scm.ensure_clean_working_directory(options.force_clean)
</span><span class="cx"> 
</span><span class="cx">     @classmethod
</span><del>-    def build_and_commit(cls, scm, options):
</del><ins>+    def build_and_commit(cls, scm, options, commit_message=None):
</ins><span class="cx">         if options.build:
</span><span class="cx">             cls.build_webkit(quiet=options.quiet)
</span><span class="cx">             if options.test:
</span><span class="cx">                 cls.run_webkit_tests(launch_safari=not options.commit_queue, quiet=options.quiet)
</span><del>-        commit_message = commit_message_for_this_commit(scm)
</del><ins>+        if not commit_message:
+            commit_message = commit_message_for_this_commit(scm)
</ins><span class="cx">         commit_log = scm.commit_with_message(commit_message.message())
</span><span class="cx">         return bug_comment_from_commit_text(scm, commit_log)
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-class LandAndUpdateBug(Command):
</del><ins>+class LandCommitsAndUpdateBug(Command):
</ins><span class="cx">     def __init__(self):
</span><span class="cx">         options = [
</span><ins>+            make_option(&quot;-b&quot;, &quot;--bug-id&quot;, action=&quot;store&quot;, type=&quot;string&quot;, dest=&quot;bug_id&quot;, help=&quot;Specify bug id if no URL is provided in the commit log.&quot;),
</ins><span class="cx">             make_option(&quot;-r&quot;, &quot;--reviewer&quot;, action=&quot;store&quot;, type=&quot;string&quot;, dest=&quot;reviewer&quot;, help=&quot;Update ChangeLogs to say Reviewed by REVIEWER.&quot;),
</span><span class="cx">         ]
</span><span class="cx">         options += WebKitLandingScripts.land_options()
</span><del>-        Command.__init__(self, 'Lands the current working directory diff and updates the bug if provided.', '[BUGID]', options=options)
</del><ins>+        Command.__init__(self, 'Lands a range of local commits and updates bugs.', 'COMMITISH', options=options, requires_local_commits=True)
</ins><span class="cx"> 
</span><del>-    def guess_reviewer_from_bug(self, bugs, bug_id):
-        patches = bugs.fetch_reviewed_patches_from_bug(bug_id)
-        if len(patches) != 1:
-            log(&quot;%s on bug %s, cannot infer reviewer.&quot; % (pluralize(&quot;reviewed patch&quot;, len(patches)), bug_id))
-            return None
-        patch = patches[0]
-        reviewer = patch['reviewer']
-        log('Guessing &quot;%s&quot; as reviewer from attachment %s on bug %s.' % (reviewer, patch['id'], bug_id))
-        return reviewer
</del><ins>+    def execute(self, options, args, tool):
+        commit_ids = tool.scm().commit_ids_from_commitish_arguments(args)
</ins><span class="cx"> 
</span><del>-    def update_changelogs_with_reviewer(self, reviewer, bug_id, tool):
-        if not reviewer:
</del><ins>+        if len(commit_ids) &gt; 10:
+            error(&quot;Are you sure you want to land %s local patches?&quot; % (pluralize('patch', len(commit_ids))))
+            # Could add a --patches-limit option.
+
+        for commit_id in commit_ids:
+            commit_message = tool.scm().commit_message_for_local_commit(commit_id)
+
+            bug_id = options.bug_id or parse_bug_id(commit_message)
</ins><span class="cx">             if not bug_id:
</span><del>-                log(&quot;No bug id provided and --reviewer= not provided.  Not updating ChangeLogs with reviewer.&quot;)
-                return
-            reviewer = self.guess_reviewer_from_bug(tool.bugs, bug_id)
</del><ins>+                log(&quot;Skipping %s: No bug id found in commit log or specified with --bug-id.&quot; % commit_id)
+                continue
</ins><span class="cx"> 
</span><del>-        if not reviewer:
-            log(&quot;Failed to guess reviewer from bug %s and --reviewer= not provided.  Not updating ChangeLogs with reviewer.&quot; % bug_id)
-            return
</del><ins>+            reviewer = options.reviewer or guess_reviewer_from_bug(tool.bugs, bug_id)
+            if not reviewer:
+                error(&quot;No reviewer available; can't update ChangeLogs.&quot;);
</ins><span class="cx"> 
</span><del>-        changelogs = modified_changelogs(tool.scm())
-        for changelog in changelogs:
-            set_reviewer_in_changelog(changelog, reviewer)
</del><ins>+            patch = {
+                'bug_id': bug_id,
+                'commit_id': commit_id,
+                'diff': tool.scm().create_patch_from_local_commit(commit_id),
+                'reviewer': reviewer,
+            }
</ins><span class="cx"> 
</span><ins>+            commit_message.replace_text(REVIEWER_PLACEHOLDER, reviewer)
+            update_changelogs_with_reviewer(reviewer, bug_id, tool)
+
+            log(&quot;Updating bug %s&quot; % bug_id)
+            comment_text = LandPatchesFromBugs.land_patches(bug_id, [patch], options, tool, commit_message)
+
+
+class LandDiffAndUpdateBug(Command):
+    def __init__(self):
+        options = [
+            make_option(&quot;-r&quot;, &quot;--reviewer&quot;, action=&quot;store&quot;, type=&quot;string&quot;, dest=&quot;reviewer&quot;, help=&quot;Update ChangeLogs to say Reviewed by REVIEWER.&quot;),
+            make_option(&quot;--no-close&quot;, action=&quot;store_false&quot;, dest=&quot;close_bug&quot;, default=True, help=&quot;Leave bug open after landing.&quot;),
+            make_option(&quot;--no-build&quot;, action=&quot;store_false&quot;, dest=&quot;build&quot;, default=True, help=&quot;Commit without building first, implies --no-test.&quot;),
+            make_option(&quot;--no-test&quot;, action=&quot;store_false&quot;, dest=&quot;test&quot;, default=True, help=&quot;Commit without running run-webkit-tests.&quot;),
+        ]
+        Command.__init__(self, 'Lands the current working directory diff and updates the bug if provided.', '[BUGID]', options=options)
+
</ins><span class="cx">     def execute(self, options, args, tool):
</span><span class="cx">         bug_id = args[0] if len(args) else None
</span><span class="cx">         os.chdir(tool.scm().checkout_root)
</span><span class="cx"> 
</span><del>-        self.update_changelogs_with_reviewer(options.reviewer, bug_id, tool)
</del><ins>+        reviewer = options.reviewer or guess_reviewer_from_bug(tool.bugs, bug_id)
+        update_changelogs_with_reviewer(reviewer, bug_id, tool)
</ins><span class="cx"> 
</span><span class="cx">         comment_text = WebKitLandingScripts.build_and_commit(tool.scm(), options)
</span><span class="cx">         if bug_id:
</span><span class="lines">@@ -357,15 +400,18 @@
</span><span class="cx">         Command.__init__(self, 'Lands all patches on a bug optionally testing them first', 'BUGID', options=options)
</span><span class="cx"> 
</span><span class="cx">     @classmethod
</span><del>-    def land_patches(cls, bug_id, patches, options, tool):
</del><ins>+    def land_patches(cls, bug_id, patches, options, tool, commit_message=None):
</ins><span class="cx">         try:
</span><span class="cx">             comment_text = &quot;&quot;
</span><span class="cx">             for patch in patches:
</span><span class="cx">                 tool.scm().update_webkit() # Update before every patch in case the tree has changed
</span><del>-                log(&quot;Applying %s from bug %s.&quot; % (patch['id'], bug_id))
</del><ins>+                log(&quot;Applying %s from bug %s.&quot; % (patch.get('id') or patch.get('commit_id'), bug_id))
</ins><span class="cx">                 tool.scm().apply_patch(patch, force=options.commit_queue)
</span><del>-                comment_text = WebKitLandingScripts.build_and_commit(tool.scm(), options)
-                tool.bugs.clear_attachment_flags(patch['id'], comment_text)
</del><ins>+                comment_text = WebKitLandingScripts.build_and_commit(tool.scm(), options, commit_message)
+                if patch.get('id'):
+                    tool.bugs.clear_attachment_flags(patch['id'], comment_text)
+                else:
+                    tool.bugs.post_comment_to_bug(patch['bug_id'], comment_text)
</ins><span class="cx"> 
</span><span class="cx">             if options.close_bug:
</span><span class="cx">                 tool.bugs.close_bug_as_fixed(bug_id, &quot;All reviewed patches have been landed.  Closing bug.&quot;)
</span><span class="lines">@@ -607,7 +653,8 @@
</span><span class="cx">             { 'name' : 'reviewed-patches', 'object' : ReviewedPatchesOnBug() },
</span><span class="cx">             { 'name' : 'create-bug', 'object' : CreateBug() },
</span><span class="cx">             { 'name' : 'apply-patches', 'object' : ApplyPatchesFromBug() },
</span><del>-            { 'name' : 'land-diff', 'object' : LandAndUpdateBug() },
</del><ins>+            { 'name' : 'land-commits', 'object' : LandCommitsAndUpdateBug() },
+            { 'name' : 'land-diff', 'object' : LandDiffAndUpdateBug() },
</ins><span class="cx">             { 'name' : 'land-patches', 'object' : LandPatchesFromBugs() },
</span><span class="cx">             { 'name' : 'commit-message', 'object' : CommitMessageForCurrentDiff() },
</span><span class="cx">             { 'name' : 'obsolete-attachments', 'object' : ObsoleteAttachmentsOnBug() },
</span></span></pre></div>
<a id="trunkWebKitToolsScriptsmodulesscmpy"></a>
<div class="modfile"><h4>Modified: trunk/WebKitTools/Scripts/modules/scm.py (47342 => 47343)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/WebKitTools/Scripts/modules/scm.py        2009-08-16 23:24:44 UTC (rev 47342)
+++ trunk/WebKitTools/Scripts/modules/scm.py        2009-08-17 00:30:53 UTC (rev 47343)
</span><span class="lines">@@ -76,7 +76,10 @@
</span><span class="cx">     def message(self):
</span><span class="cx">         return &quot;\n&quot;.join(self.message_lines) + &quot;\n&quot;
</span><span class="cx"> 
</span><ins>+    def replace_text(self, placeholder, new_value):
+        self.message_lines = map(lambda line: line.replace(placeholder, new_value), self.message_lines)
</ins><span class="cx"> 
</span><ins>+
</ins><span class="cx"> class ScriptError(Exception):
</span><span class="cx">     pass
</span><span class="cx"> 
</span><span class="lines">@@ -123,15 +126,22 @@
</span><span class="cx">     def apply_patch(self, patch, force=False):
</span><span class="cx">         # It's possible that the patch was not made from the root directory.
</span><span class="cx">         # We should detect and handle that case.
</span><del>-        curl_process = subprocess.Popen(['curl', '--silent', '--show-error', patch['url']], stdout=subprocess.PIPE)
</del><ins>+        patch_apply_process = None
</ins><span class="cx">         args = [self.script_path('svn-apply'), '--reviewer', patch['reviewer']]
</span><span class="cx">         if force:
</span><span class="cx">             args.append('--force')
</span><del>-        patch_apply_process = subprocess.Popen(args, stdin=curl_process.stdout)
</del><ins>+        if patch.get('url'):
+            curl_process = subprocess.Popen(['curl', '--silent', '--show-error', patch['url']], stdout=subprocess.PIPE)
+            patch_apply_process = subprocess.Popen(args, stdin=curl_process.stdout)
+        elif patch.get('diff'):
+            patch_apply_process = subprocess.Popen(args, stdin=subprocess.PIPE)
+            patch_apply_process.communicate(patch['diff'])
+        else:
+            error(&quot;Unknown patch object.&quot;)
</ins><span class="cx"> 
</span><span class="cx">         return_code = patch_apply_process.wait()
</span><span class="cx">         if return_code:
</span><del>-            raise ScriptError(&quot;Patch %s from bug %s failed to download and apply.&quot; % (patch['url'], patch['bug_id']))
</del><ins>+            raise ScriptError(&quot;Patch %s from bug %s failed to download and apply.&quot; % (patch.get('url') or patch.get('commit_id'), patch['bug_id']))
</ins><span class="cx"> 
</span><span class="cx">     def run_status_and_extract_filenames(self, status_command, status_regexp):
</span><span class="cx">         filenames = []
</span><span class="lines">@@ -375,7 +385,7 @@
</span><span class="cx">             if '...' in commitish:
</span><span class="cx">                 raise ScriptError(&quot;'...' is not supported (found in '%s'). Did you mean '..'?&quot; % commitish)
</span><span class="cx">             elif '..' in commitish:
</span><del>-                commit_ids += self.run_command(['git', 'rev-list', commitish]).splitlines()
</del><ins>+                commit_ids += reversed(self.run_command(['git', 'rev-list', commitish]).splitlines())
</ins><span class="cx">             else:
</span><span class="cx">                 # Turn single commits or branch or tag names into commit ids.
</span><span class="cx">                 commit_ids += self.run_command(['git', 'rev-parse', '--revs-only', commitish]).splitlines()
</span></span></pre>
</div>
</div>

</body>
</html>