Changeset 17 for mergebot/trunk
- Timestamp:
- Jun 8, 2009 3:07:47 AM (15 years ago)
- Location:
- mergebot/trunk
- Files:
-
- 7 added
- 2 deleted
- 8 edited
- 4 moved
Legend:
- Unmodified
- Added
- Removed
-
mergebot/trunk/COPYING
r16 r17 2 2 3 3 Copyright (c) 2007 CommProve, Inc. 4 Copyright (c) 2008-2009 Eli Carter <eli.carter@retracile.net> 4 5 5 6 Permission is hereby granted, free of charge, to any person obtaining a copy -
mergebot/trunk/README.txt
r16 r17 1 1 = Installing = 2 2 # Checkout the source. 3 $ svn co <url> mergebot-0.1 04 $ cd mergebot-0.1 05 $ python setup.py bdist_egg6 $ cp dist/TracMergeBot*.egg <mytracenv>/plugins/3 $ svn co <url> mergebot-0.11 4 $ cd mergebot-0.11 5 $ ./rpm/makerpm 6 $ su -c "rpm --install dist/TracMergeBot*.noarch.rpm" 7 7 # Create the mergebot work area 8 8 $ mkdir <mytracenv>/mergebot 9 9 $ chown <webserver>:<webserver> <mytracenv>/mergebot 10 10 11 # Add to trac.ini: 12 [mergebot] 13 work_dir = /var/www/trac/mytracenv/mergebot 14 repository_url = http://servername/repos 11 Enable mergebot in the [components] section: 12 mergebot.web_ui.* = enabled 15 13 16 [ticket-custom] 17 mergebotstate = select 18 mergebotstate.label = MergeBotState 19 mergebotstate.options = | tomerge | merged | tobranch | branched | conflicts 20 mergebotstate.value = 14 If you want to use the ticket workflow features you will also need to change 15 [components] 16 mergebot.ticket_actions.* = enabled 17 [ticket] 18 workflow = ConfigurableTicketWorkflow,MergebotActionController 19 Restart your webserver. 20 And then use "mergebot" in the .operations section of the relevant ticket actions. 21 21 22 # Give MERGEBOT_* permissions to your users. 23 # Make sure you have a version for 'trunk' 24 # Make sure you have your Subversion directory structure setup as 25 # <component>/trunk 26 # <component>/tags 27 # <component>/branches 22 # Add needed entries to trac.ini: 23 $ trac-admin <mytracenv> upgrade 24 Then edit <mytracenv>/conf/trac.ini again. 25 In particular, look for the [mergebot] section that the upgrade added, and be sure the repository_url is set. 26 Give MERGEBOT_* permissions to your users. 28 27 29 # Restart your webserver 28 Be sure you have a version named 'trunk' -- and you will likely want to make that the default. 29 Mergebot assumes that each component is the name of a top-level directory in the svn repository, and that the versions are 'trunk', a ticket number with a preceeding '#', or a release name with a corresponding 'branches/release-XYZ' directory. 30 -
mergebot/trunk/mergebot/BranchActor.py
r16 r17 1 1 #!/usr/bin/env python 2 2 """Module for creating new branches for tickets""" 3 # Syntax: BranchActor.py ticketnum component version requestor4 3 5 4 import os 6 import sys7 5 import time 8 import trac.env9 6 10 import SvnOps 11 from WorkQueue import MergeBotActor, VersionToDir 12 from TrackerTools import GetRepositoryPublicUrl, GetRepositoryLocalUrl, Task, \ 13 GetLogFile 7 from mergebot import SvnOps 8 from mergebot.Actor import Actor 14 9 15 def branch_action(trac_env, ticketnum, component, version, requestor): 16 "Create the branch" 17 task_obj = Task(trac_env, ticketnum) 10 class BranchActor(Actor): 11 """This class handles creating a new branch for a ticket.""" 12 def execute(self): 13 """Create the branch for the given ticket. 14 """ 15 results = {} 16 # Setup logging 17 logfile = self.logfilename() 18 open(logfile, "a").write("%s: branching ticket %s\n" % (time.asctime(), 19 self.ticket)) 18 20 19 # Setup logging 20 logfile = GetLogFile(trac_env, ticketnum) 21 open(logfile, "a").write("%s: branching ticket %s\n" % (time.asctime(), 22 ticketnum)) 21 # Make sure the various urls we require do exist 22 if not SvnOps.get_branch_info(self.local_url(), logfile): 23 comment = 'Component %s does not exist in the repository.' \ 24 % self.component 25 return results, comment, False 26 if not SvnOps.get_branch_info(self.local_url() + '/branches', logfile): 27 comment = 'No directory in which to create branches for component %s in the repository.' % self.component 28 return results, comment, False 29 if not SvnOps.get_branch_info(self.baseline_local_url(), logfile): 30 comment = 'Version %s for component %s does not exist in the repository.' % (self.version, self.component) 31 return results, comment, False 23 32 24 # Determine the URLS to copy from and to 25 branchdir = "branches/ticket-%s" % (ticketnum) 26 copyfrom = os.path.join(GetRepositoryLocalUrl(trac_env), component, 27 VersionToDir(version)) 28 copyto = os.path.join(GetRepositoryLocalUrl(trac_env), component, branchdir) 33 commit_header = 'Ticket #%s: %s' % (self.ticket, self.summary) 29 34 30 commit_header = "Ticket #%s: %s" % (ticketnum, task_obj.GetSummary()) 35 # Delete the branch if it already exists. This can happen if the branch 36 # was merged, but we're still working on it. 37 if SvnOps.get_branch_info(self.branch_local_url(), logfile): 38 # This branch already exists. 39 commit_message = "\n".join([commit_header, 40 " Delete old branch", 41 ]) 42 new_rev = SvnOps.delete_branch(self.branch_local_url(), 43 commit_message, logfile) 44 if new_rev == -1: 45 results['mergebotstate'] = 'branchfailed' 46 comment = 'Deleting the existing branch failed.' 47 return results, comment, False 31 48 32 # Delete the branch if it already exists. This can happen if the branch 33 # was merged, but we're still working on it. 34 if SvnOps.get_branch_info(copyto, logfile): 35 # This branch already exists. 49 # Do the branch creationg 36 50 commit_message = "\n".join([commit_header, 37 " Delete old branch", 51 " Create branch from %s for %s." % (self.version, 52 self.requestor), 38 53 ]) 39 new_rev = SvnOps.delete_branch(copyto, commit_message, logfile) 40 if new_rev == -1: 41 status = "branchfailed" 42 return status, task_obj 43 44 # Do the branch creationg 45 commit_message = "\n".join([commit_header, 46 " Create branch from %s for %s." % (version, requestor), 47 ]) 48 retval = SvnOps.create_branch(copyfrom, copyto, commit_message, logfile) 49 if retval: 50 # Failed for some reason. 51 status = "branchfailed" 52 else: 53 publiccopyto = os.path.join(GetRepositoryPublicUrl(trac_env), component, 54 branchdir) 55 comment = "\n".join([ 56 "Created branch from %s for %s." % (version, requestor), 57 "", 58 "Browse branch [source:%s source code] and [log:%s commit log]." % 59 (os.path.join(component, branchdir), 60 os.path.join(component, branchdir)), 61 "", 62 "To checkout, run:", 63 "{{{", 64 "svn checkout %s %s-%s" % (publiccopyto, component, ticketnum), 65 "}}}", 54 retval = SvnOps.create_branch(self.baseline_local_url(), 55 self.branch_local_url(), commit_message, logfile) 56 if retval: 57 # Failed for some reason. 58 results['mergebotstate'] = 'branchfailed' 59 comment = 'Failed to create branch.' 60 return results, comment, False 61 results['mergebotstate'] = 'branched' 62 comment = '\n'.join([ 63 'Created branch from %s for %s.' % (self.version, self.requestor), 64 '', 65 'Browse branch [source:%s/branches/ticket-%s source code] and [log:%s/branches/ticket-%s commit log].' % 66 (self.component, self.ticket, self.component, self.ticket), 67 '', 68 'To checkout, run:', 69 '{{{', 70 'svn checkout %s %s-%s' % (self.branch_public_url(), 71 self.component, self.ticket), 72 '}}}', 66 73 ]) 67 task_obj.AddComment(comment) 68 status = "branched" 69 return status, task_obj 70 71 class BranchActor(MergeBotActor): 72 "Actor for creating a new branch." 73 def __init__(self, trac_env): 74 MergeBotActor.__init__(self, trac_env, "branch", branch_action) 75 76 def main(): 77 tracdir = sys.argv[1] 78 trac_env = trac.env.open_environment(tracdir) 79 branchingActor = BranchActor(trac_env) 80 branchingActor.AddTask(sys.argv[2:]) 81 branchingActor.Run() 82 83 if __name__ == "__main__": 84 main() 74 return results, comment, True 85 75 86 76 # vim:foldcolumn=4 foldmethod=indent -
mergebot/trunk/mergebot/CheckMergeActor.py
r16 r17 1 1 #!/usr/bin/env python 2 2 """ 3 Syntax: MergeActor.py ticketnum component version requestor4 5 3 Verify that a branch can be merged to its trunk without conflicts, but don't 6 4 commit the merge. … … 8 6 9 7 import os 10 import sys 11 import trac.env 8 import shutil 12 9 13 import SvnOps 14 from WorkQueue import MergeBotActor, VersionToDir 15 from TrackerTools import GetWorkDir, GetRepositoryLocalUrl, Task, GetLogFile 10 from mergebot import SvnOps 11 from mergebot.Actor import Actor 16 12 17 def check_merge_action(trac_env, ticketnum, component, version, requestor): 13 class CheckMergeActor(Actor): 14 """Checks that this ticket can be merged to its baseline, but don't modify 15 the repository. 18 16 """ 19 Verify that a branch can be merged to its trunk without conflicts, but 20 don't commit the merge. 21 """ 22 task_obj = Task(trac_env, ticketnum) 23 endstatus = "???????" 24 workdir = GetWorkDir(trac_env, ticketnum, __name__) 25 logfile = GetLogFile(trac_env, ticketnum) 26 # FIXME: Should we just bail out instead? 27 if os.path.exists(workdir): 28 os.system("rm -rf \"%s\"" % (workdir)) 17 def execute(self): 18 """ 19 Verify that a branch can be merged to its trunk without conflicts, but 20 don't commit the merge. 21 """ 22 results = {} 23 workdir = self.work_dir 24 logfile = self.logfilename() 29 25 30 sourceurl = os.path.join(GetRepositoryLocalUrl(trac_env), component, 31 VersionToDir(version)) 32 ticketurl = os.path.join(GetRepositoryLocalUrl(trac_env), component, "branches", 33 "ticket-%s" % ticketnum) 26 if os.path.exists(workdir): 27 shutil.rmtree(workdir) 34 28 35 branch_info = SvnOps.get_branch_info(ticketurl, GetLogFile(trac_env, ticketnum)) 36 # FIXME: if not branch_info: # Error case 37 startrev, endrev = branch_info 29 # Make sure the various urls we require do exist 30 if not SvnOps.get_branch_info(self.local_url(), logfile): 31 comment = 'Component %s does not exist in the repository.' \ 32 % self.component 33 return results, comment, False 34 if not SvnOps.get_branch_info(self.local_url() + '/branches', logfile): 35 comment = 'No directory in which to create branches for ' \ 36 'component %s in the repository.' % self.component 37 return results, comment, False 38 if not SvnOps.get_branch_info(self.baseline_local_url(), logfile): 39 comment = 'Version %s for component %s does not exist in the ' \ 40 'repository.' % (self.version, self.component) 41 return results, comment, False 38 42 39 SvnOps.checkout(sourceurl, workdir, logfile) 40 # TODO: check return code of the above 41 results = SvnOps.merge(ticketurl, workdir, (startrev, endrev), logfile) 42 conflicts = SvnOps.conflicts_from_merge_results(results) 43 if conflicts: 44 message = "\n".join([ 45 "Found %s conflicts while checking merge of %s:%s to %s for %s." % \ 46 (len(conflicts), startrev, endrev, version, requestor), 47 "Files in conflict:", 48 "{{{", 49 "\n".join(conflicts), 50 "}}}", 51 "A rebranch will be needed before this can be merged.", 52 ]) 53 endstatus = "conflicts" 54 else: 55 message = \ 56 "Found no conflicts while checking merge of %s:%s to %s for %s." \ 57 % (startrev, endrev, version, requestor) 58 endstatus = "branched" 43 branch_info = SvnOps.get_branch_info(self.branch_local_url(), logfile) 44 if not branch_info: 45 comment = 'Branch for ticket %s does not exist in the repository.' \ 46 % (self.ticket) 47 return results, comment, False 48 startrev, endrev = branch_info 59 49 60 # Clean up the work area 61 os.system("rm -rf \"%s\"" % (workdir, )) 50 SvnOps.checkout(self.baseline_local_url(), workdir, logfile) 51 # TODO: check return code of the above 52 merge_results = SvnOps.merge(self.branch_local_url(), workdir, 53 (startrev, endrev), logfile) 54 conflicts = SvnOps.conflicts_from_merge_results(merge_results) 55 if conflicts: 56 message = '\n'.join([ 57 'Found %s conflicts while checking merge of %s:%s to %s for ' \ 58 '%s.' % (len(conflicts), startrev, endrev, self.version, 59 self.requestor), 60 'Files in conflict:', 61 '{{{', 62 '\n'.join(conflicts), 63 '}}}', 64 'A rebranch will be needed before this can be merged.', 65 ]) 66 success = False 67 else: 68 message = 'Found no conflicts while checking merge of %s:%s to ' \ 69 '%s for %s.' % (startrev, endrev, self.version, self.requestor) 70 success = True 62 71 63 task_obj.AddComment(message) 64 return endstatus, task_obj 72 # Clean up the work area 73 if os.path.exists(workdir): 74 shutil.rmtree(workdir) 65 75 66 class CheckMergeActor(MergeBotActor): 67 "Actor wrapper for the check_merge_action" 68 def __init__(self, trac_env): 69 MergeBotActor.__init__(self, trac_env, "checkmerge", check_merge_action) 70 71 def main(): 72 tracdir = sys.argv[1] 73 trac_env = trac.env.open_environment(tracdir) 74 mergingActor = CheckMergeActor(trac_env) 75 mergingActor.AddTask(sys.argv[1:]) 76 mergingActor.Run() 77 78 if __name__ == "__main__": 79 main() 76 return results, message, success 80 77 81 78 # vim:foldcolumn=4 foldmethod=indent -
mergebot/trunk/mergebot/MergeActor.py
r16 r17 7 7 8 8 import os 9 import sys 10 import trac.env 9 import shutil 11 10 12 import SvnOps 13 from WorkQueue import MergeBotActor, VersionToDir 14 from TrackerTools import GetWorkDir, GetRepositoryLocalUrl, Task, GetLogFile 11 from mergebot import SvnOps 12 from mergebot.Actor import Actor 15 13 16 def merge_action(trac_env, ticketnum, component, version, requestor): 17 "Merge a branch to its trunk" 18 task_obj = Task(trac_env, ticketnum) 19 logfile = GetLogFile(trac_env, ticketnum) 14 class MergeActor(Actor): 15 """Merges a branch to the line of development on which it is based. 16 """ 17 def execute(self): 18 "Merge a branch to its trunk" 19 results = {} 20 logfile = self.logfilename() 21 checkoutdir = self.work_dir 22 # Delete the working directory so we get a completely clean working 23 # copy. 24 if os.path.exists(checkoutdir): 25 shutil.rmtree(checkoutdir) 20 26 21 checkoutdir = GetWorkDir(trac_env, ticketnum, __name__) 22 # FIXME: Should we just bail out instead? 23 if os.path.exists(checkoutdir): 24 os.system("rm -rf \"%s\" >>%s 2>&1" % (checkoutdir, logfile)) 27 # Make sure the various urls we require do exist 28 if not SvnOps.get_branch_info(self.local_url(), logfile): 29 comment = 'Component %s does not exist in the repository.' \ 30 % self.component 31 return results, comment, False 32 if not SvnOps.get_branch_info(self.local_url() + '/branches', logfile): 33 comment = 'No directory in which to create branches for component %s in the repository.' % self.component 34 return results, comment, False 35 if not SvnOps.get_branch_info(self.baseline_local_url(), logfile): 36 comment = 'Version %s for component %s does not exist in the repository.' % (self.version, self.component) 37 return results, comment, False 25 38 26 sourceurl = os.path.join(GetRepositoryLocalUrl(trac_env), component, 27 VersionToDir(version)) 28 ticketurl = os.path.join(GetRepositoryLocalUrl(trac_env), component, "branches", 29 "ticket-%s" % ticketnum) 39 rev_info = SvnOps.get_branch_info(self.branch_local_url(), logfile) 40 if not rev_info: 41 comment = 'Branch for ticket %s does not exist in the repository.' % (self.ticket) 42 return results, comment, False 43 startrev, endrev = rev_info 30 44 31 # FIXME: needs error checking 32 startrev, endrev = SvnOps.get_branch_info(ticketurl, logfile) 45 SvnOps.checkout(self.baseline_local_url(), checkoutdir, logfile) 46 # FIXME: check return code 47 merge_results = SvnOps.merge(self.branch_local_url(), checkoutdir, 48 (startrev, endrev), logfile) 49 conflicts = SvnOps.conflicts_from_merge_results(merge_results) 50 if conflicts: 51 comment = "\n".join([ 52 "Found %s conflicts in attempt to merge %s:%s to %s for %s." % \ 53 (len(conflicts), startrev, endrev, self.version, 54 self.requestor), 55 "Files in conflict:", 56 "{{{", 57 "\n".join(conflicts), 58 "}}}", 59 "A rebranch will be needed before this can be merged.", 60 ]) 61 results['mergebotstate'] = 'conflicts' 62 success = False 63 else: 64 # The merge worked, so commit the changes. 65 commitmessage = "\n".join([ 66 "Ticket #%s: %s" % (self.ticket, self.summary), 67 " Merge of %s:%s to %s for %s." % (startrev, endrev, 68 self.version, self.requestor), 69 ]) 70 committedrev = SvnOps.commit(checkoutdir, commitmessage, logfile) 71 # Sed message and endstatus 72 if committedrev == None: 73 # Apparently nothing to commit. 74 comment = "\n".join([ 75 "Merged %s:%s to %s for %s." % (startrev, endrev, 76 self.version, self.requestor), 77 "No changes to commit.", 78 ]) 79 results['mergebotstate'] = 'merged' 80 success = True 81 elif committedrev >= 0: 82 # The commit succeeded. 83 comment = "\n".join([ 84 "Merged %s:%s to %s for %s." % (startrev, endrev, 85 self.version, self.requestor), 86 "Changeset [%s]. [source:%s/%s@%s]" % (committedrev, 87 self.component, self.version_subdir(), committedrev), 88 ]) 89 results['mergebotstate'] = 'merged' 90 success = True 91 else: 92 # The commit for the merge failed. 93 comment = \ 94 "Commit failed in attempt to merge %s:%s to %s for %s." \ 95 % (startrev, endrev, self.version, self.requestor) 96 #results['mergebotstate'] = 'mergefailed' 97 success = False 33 98 34 SvnOps.checkout(sourceurl, checkoutdir, logfile) 35 # FIXME: check return code 36 results = SvnOps.merge(ticketurl, checkoutdir, (startrev, endrev), logfile) 37 conflicts = SvnOps.conflicts_from_merge_results(results) 38 if conflicts: 39 message = "\n".join([ 40 "Found %s conflicts in attempt to merge %s:%s to %s for %s." % \ 41 (len(conflicts), startrev, endrev, version, requestor), 42 "Files in conflict:", 43 "{{{", 44 "\n".join(conflicts), 45 "}}}", 46 "A rebranch will be needed before this can be merged.", 47 ]) 48 endstatus = "conflicts" 49 else: 50 # The merge worked, so commit the changes. 51 summary = task_obj.GetSummary() 52 commitmessage = "\n".join([ 53 "Ticket #%s: %s" % (ticketnum, summary), 54 " Merge of %s:%s to %s for %s." % (startrev, endrev, version, 55 requestor), 56 ]) 57 committedrev = SvnOps.commit(checkoutdir, commitmessage, logfile) 58 # Sed message and endstatus 59 if committedrev == None: 60 # Apparently nothing to commit. 61 message = "\n".join([ 62 "Merged %s:%s to %s for %s." % (startrev, endrev, version, 63 requestor), 64 "No changes to commit.", 65 ]) 66 endstatus = "merged" 67 elif committedrev >= 0: 68 # The commit succeeded. 69 message = "\n".join([ 70 "Merged %s:%s to %s for %s." % (startrev, endrev, version, 71 requestor), 72 "Changeset [%s]. [source:%s/%s@%s]" % (committedrev, 73 component, VersionToDir(version), committedrev), 74 ]) 75 endstatus = "merged" 76 else: 77 # The commit for the merge failed. 78 message = "Commit failed in attempt to merge %s:%s to %s for %s." \ 79 % (startrev, endrev, version, requestor) 80 endstatus = "mergefailed" 99 # Clean up the work area 100 if os.path.exists(checkoutdir): 101 shutil.rmtree(checkoutdir) 81 102 82 # Clean up the work area 83 os.system("rm -rf \"%s\" >>%s 2>&1" % (checkoutdir, logfile)) 84 85 task_obj.AddComment(message) 86 return endstatus, task_obj 87 88 class MergeActor(MergeBotActor): 89 "Actor wrapper for merge_action" 90 def __init__(self, trac_env): 91 MergeBotActor.__init__(self, trac_env, "merge", merge_action) 92 93 def main(): 94 tracdir = sys.argv[1] 95 trac_env = trac.env.open_environment(tracdir) 96 mergingActor = MergeActor(trac_env) 97 mergingActor.AddTask(sys.argv[2:]) 98 mergingActor.Run() 99 100 if __name__ == "__main__": 101 main() 103 return results, comment, success 102 104 103 105 # vim:foldcolumn=4 foldmethod=indent -
mergebot/trunk/mergebot/RebranchActor.py
r16 r17 1 1 #!/usr/bin/env python 2 2 """ 3 Syntax: RebranchActor.py ticketnum component version requestor4 5 3 Rebranch a branch from its trunk, pulling in the changes made on the branch if 6 4 possible. 7 5 """ 8 6 7 import shutil 8 import time 9 9 import os 10 import sys11 import time12 import trac.env13 10 14 import SvnOps 15 from WorkQueue import MergeBotActor, VersionToDir 16 from TrackerTools import GetRepositoryPublicUrl, GetRepositoryLocalUrl, Task, \ 17 GetWorkDir, GetLogFile 11 from mergebot import SvnOps 12 from mergebot.Actor import Actor 18 13 19 def rebranch_action(trac_env, ticketnum, component, version, requestor): 14 class RebranchActor(Actor): 15 """Rebranches a ticket from its baseline. 20 16 """ 21 To rebranch from the baseline, you have to do these steps:22 delete the branch.23 recreate the branch.24 merge in the changes from the deletedbranch.25 if there are no conflicts, commit those changes tothe branch.26 """27 # FIXME: This desparately needs to be refactored.28 # Do as much work as we can before affecting the repository so we have as29 # little cleanup as possible and the fewest cases where we leave a broken30 # branch.31 task_obj = Task(trac_env, ticketnum)32 summary = task_obj.GetSummary()17 def execute(self): 18 """ 19 To rebranch from the baseline, you have to do these steps: 20 delete the branch. 21 recreate the branch. 22 merge in the changes from the deleted branch. 23 if there are no conflicts, commit those changes to the branch. 24 """ 25 # FIXME: This desparately needs to be refactored. 26 # Do as much work as we can before affecting the repository so we have 27 # as little cleanup as possible and the fewest cases where we leave a 28 # broken branch. 33 29 34 # We need to improve the logging of the rebranch stuff. 35 logfile = GetLogFile(trac_env, ticketnum) 36 open(logfile, "a").write("%s rebranching ticket %s\n" % (time.asctime(), 37 ticketnum)) 30 results = {} 38 31 39 baserepositoryurl = str(GetRepositoryLocalUrl(trac_env)) 40 branchurl = os.path.join(baserepositoryurl, component, "branches", 41 "ticket-%s" % ticketnum) 42 baselineurl = os.path.join(baserepositoryurl, component, 43 VersionToDir(version)) 44 publicbranchurl = os.path.join(GetRepositoryPublicUrl(trac_env), component, 45 "branches", "ticket-%s" % ticketnum) 32 # We need to improve the logging of the rebranch stuff. 33 logfile = self.logfilename() 34 open(logfile, "a").write("%s rebranching ticket %s\n" % 35 (time.asctime(), self.ticket)) 46 36 47 # Determine the revision range for the branch: startrev:endrev 48 branchinfo = SvnOps.get_branch_info(branchurl, logfile) 49 if not branchinfo: 50 open(logfile, "a").write( 51 "Unable to get %s branch revision information.\n" % (branchurl)) 52 return "rebranchfailed", task_obj 53 startrev, endrev = branchinfo 37 # Make sure the various urls we require do exist 38 if not SvnOps.get_branch_info(self.local_url(), logfile): 39 comment = 'Component %s does not exist in the repository.' \ 40 % self.component 41 return results, comment, False 42 if not SvnOps.get_branch_info(self.local_url() + '/branches', logfile): 43 comment = 'No directory in which to create branches for ' \ 44 'component %s in the repository.' % self.component 45 return results, comment, False 46 if not SvnOps.get_branch_info(self.baseline_local_url(), logfile): 47 comment = 'Version %s for component %s does not exist in the ' \ 48 'repository.' % (self.version, self.component) 49 return results, comment, False 54 50 55 workingcopy = GetWorkDir(trac_env, ticketnum, __name__) 51 rev_info = SvnOps.get_branch_info(self.branch_local_url(), logfile) 52 if not rev_info: 53 comment = \ 54 'Branch for ticket %s does not exist in the repository.' % \ 55 (self.ticket) 56 return results, comment, False 57 startrev, endrev = rev_info 56 58 57 svnmergecommand = "svn merge -r %s:%s %s@%s" % (startrev, endrev, 58 branchurl, startrev) 59 publicsvnmergecommand = "svn merge -r %s:%s %s@%s" % (startrev, endrev, 60 publicbranchurl, startrev) 61 # This is used in the ticket comments. 62 instructioncomment = "\n".join([ 63 "You will need to fix this manually by creating the branch and " 64 "then doing the merge with this command:", 65 "{{{", 66 publicsvnmergecommand, 67 "}}}", 68 ]) 69 # These are used in the commit messages. 70 rmmessage = "\n".join([ 71 "Ticket #%s: %s" % (ticketnum, summary), 72 " Remove the branch to rebranch from %s for %s." % \ 73 (version, requestor), 74 ]) 75 copymessage = "\n".join([ 76 "Ticket #%s: %s" % (ticketnum, summary), 77 " Recreate branch from %s for %s." % (version, requestor), 78 "[log:%s/branches/ticket-%s@%s Previous log]." % \ 79 (component, ticketnum, endrev), 80 ]) 81 # This is a list of commands. Each of these must complete successfully, in 82 # this order, to complete the rebranch. On failure, the given comment 83 # needs to be added to the ticket log. 84 commanderrors = ( 85 (lambda x: SvnOps.logcmd("rm -rf \"%s\"" % (workingcopy), x), 86 "Rebranch internal error: Unable to cleanup work area."), 87 (lambda x: SvnOps.delete_branch(branchurl, rmmessage, x) == -1, 88 "Rebranch internal error: Unable to delete the old branch."), 89 (lambda x: SvnOps.create_branch(baselineurl, branchurl, copymessage, x), 90 "Rebranch internal error: Unable to recreate the branch. %s" % \ 91 (instructioncomment, )), 92 (lambda x: SvnOps.checkout(branchurl, workingcopy, x), 93 "Rebranch internal error: Unable to get working copy. %s" % \ 94 (instructioncomment, )), 95 ) 96 for cmd, errorcomment in commanderrors: 97 retval = cmd(logfile) 98 if retval: 99 task_obj.AddComment(errorcomment) 100 os.system("rm -rf \"%s\"" % (workingcopy, )) 101 return "rebranchfailed", task_obj 59 workingcopy = self.work_dir 60 if os.path.exists(workingcopy): 61 shutil.rmtree(workingcopy) 102 62 103 # On success, we're in the same state as if we had just branched. 104 status = "branched" 105 # Go ahead and try to do the merge. If we got lucky and there are no 106 # conflicts, commit the changes. 107 # Put a full update on the ticket. 108 results = SvnOps.merge("%s@%s" % (branchurl, startrev), workingcopy, 109 (startrev, endrev), logfile) 110 conflicts = SvnOps.conflicts_from_merge_results(results) 111 if conflicts: 112 ticketmessage = "\n".join([ 113 "There were conflicts on rebranching.", 114 "Files in conflict:", 115 "{{{", 116 "\n".join(conflicts), 117 "}}}", 118 "You will need to resolve the conflicts manually.", 119 "To do so, update a working copy to the branch, " 120 "and run this merge command:", 121 "{{{", 122 publicsvnmergecommand, 123 "}}}", 124 "Once you have resolved the conflicts, " 125 "commit your work to the branch.", 126 ]) 127 else: # No conflicts, do the commit. 128 mergemessage = "\n".join([ 129 "Ticket #%s: %s" % (ticketnum, summary), 130 " Merge in changes from old branch for %s." % (requestor), 131 " %s" % (svnmergecommand), 132 ]) 133 newrev = SvnOps.commit(workingcopy, mergemessage, logfile) 134 if newrev == None: 135 ticketmessage = "\n".join([ 136 "Rebranched from %s for %s." % (version, requestor), 137 "There were no changes to commit to the branch.", 138 "You will need to update your working copy.", 139 ]) 140 elif newrev < 0: 141 ticketmessage = "\n".join([ 142 "Rebranch internal error: Unable to commit merged changes.", 143 "You will need to fix this manually by doing the merge with " 144 "this command:", 63 svnmergecommand = "svn merge -r %s:%s %s@%s" % (startrev, endrev, 64 self.branch_local_url(), startrev) 65 publicsvnmergecommand = "svn merge -r %s:%s %s@%s" % (startrev, endrev, 66 self.branch_public_url(), startrev) 67 # This is used in the ticket comments when there are conflicts. 68 instructioncomment = "\n".join([ 69 "You will need to fix this manually by creating the branch and " 70 "then doing the merge with this command:", 145 71 "{{{", 146 72 publicsvnmergecommand, 147 73 "}}}", 74 ]) 75 # These are used in the commit messages. 76 rmmessage = "\n".join([ 77 "Ticket #%s: %s" % (self.ticket, self.summary), 78 " Remove the branch to rebranch from %s for %s." % \ 79 (self.version, self.requestor), 80 ]) 81 copymessage = "\n".join([ 82 "Ticket #%s: %s" % (self.ticket, self.summary), 83 " Recreate branch from %s for %s." % (self.version, 84 self.requestor), 85 "[log:%s/branches/ticket-%s@%s Previous log]." % \ 86 (self.component, self.ticket, endrev), 87 ]) 88 # This is a list of commands. Each of these must complete 89 # successfully, in this order, to complete the rebranch. On failure, 90 # the given comment needs to be added to the ticket log. 91 commanderrors = ( 92 (lambda x: SvnOps.delete_branch(self.branch_local_url(), rmmessage, 93 x) == -1, 94 "Rebranch internal error: Unable to delete the old branch."), 95 (lambda x: SvnOps.create_branch(self.baseline_local_url(), 96 self.branch_local_url(), copymessage, x), 97 "Rebranch internal error: Unable to recreate the branch. %s" \ 98 % (instructioncomment, )), 99 (lambda x: SvnOps.checkout(self.branch_local_url(), workingcopy, x), 100 "Rebranch internal error: Unable to get working copy. %s" % \ 101 (instructioncomment, )), 102 ) 103 for cmd, error_comment in commanderrors: 104 retval = cmd(logfile) 105 if retval: 106 if os.path.exists(workingcopy): 107 shutil.rmtree(workingcopy) 108 results['mergebotstate'] = 'rebranchfailed' 109 return results, error_comment, False 110 111 # On success, we're in the same state as if we had just branched. 112 results['mergebotstate'] = 'branched' 113 success = True 114 115 # Go ahead and try to do the merge. If we got lucky and there are no 116 # conflicts, commit the changes. 117 # Put a full update on the ticket. 118 merge_results = SvnOps.merge("%s@%s" % (self.branch_local_url(), 119 startrev), workingcopy, (startrev, endrev), logfile) 120 conflicts = SvnOps.conflicts_from_merge_results(merge_results) 121 if conflicts: 122 ticketmessage = "\n".join([ 123 "There were conflicts on rebranching.", 124 "Files in conflict:", 125 "{{{", 126 "\n".join(conflicts), 127 "}}}", 128 "You will need to resolve the conflicts manually.", 129 "To do so, update a working copy to the branch, " 130 "and run this merge command:", 131 "{{{", 132 publicsvnmergecommand, 133 "}}}", 134 "Once you have resolved the conflicts, " 135 "commit your work to the branch.", 136 ]) 137 else: # No conflicts, do the commit. 138 mergemessage = "\n".join([ 139 "Ticket #%s: %s" % (self.ticket, self.summary), 140 " Merge in changes from old branch for %s." % self.requestor, 141 " %s" % svnmergecommand, 148 142 ]) 149 status = "rebranchfailed" 150 else: 151 ticketmessage = "\n".join([ 152 "Rebranched from %s for %s." % (version, requestor), 153 "There were no conflicts, so the changes were automatically " 154 "merged and committed to the branch.", 155 "You will need to update your working copy.", 156 ]) 143 newrev = SvnOps.commit(workingcopy, mergemessage, logfile) 144 if newrev == None: 145 ticketmessage = "\n".join([ 146 "Rebranched from %s for %s." % (self.version, 147 self.requestor), 148 "There were no changes to commit to the branch.", 149 "You will need to update your working copy.", 150 ]) 151 elif newrev < 0: 152 ticketmessage = "\n".join([ 153 "Rebranch internal error: Unable to commit merged " 154 "changes.", 155 "You will need to fix this manually by doing the merge " 156 "with this command:", 157 "{{{", 158 publicsvnmergecommand, 159 "}}}", 160 ]) 161 results['mergebotstate'] = 'rebranchfailed' 162 success = False 163 else: 164 ticketmessage = "\n".join([ 165 "Rebranched from %s for %s." % (self.version, 166 self.requestor), 167 "There were no conflicts, so the changes were " 168 "automatically merged and committed to the branch.", 169 "You will need to update your working copy.", 170 ]) 157 171 158 task_obj.AddComment( ticketmessage ) 159 os.system("rm -rf \"%s\"" % (workingcopy, )) 160 return status, task_obj 161 162 class RebranchActor(MergeBotActor): 163 "Actor wrapper for rebranch_action" 164 def __init__(self, trac_env): 165 MergeBotActor.__init__(self, trac_env, "rebranch", rebranch_action) 166 167 def main(): 168 tracdir = sys.argv[1] 169 trac_env = trac.env.open_environment(tracdir) 170 rebranchingActor = RebranchActor(trac_env) 171 rebranchingActor.AddTask(sys.argv[2:]) 172 rebranchingActor.Run() 173 174 if __name__ == "__main__": 175 main() 172 if os.path.exists(workingcopy): 173 shutil.rmtree(workingcopy) 174 return results, ticketmessage, success 176 175 177 176 # vim:foldcolumn=4 foldmethod=indent -
mergebot/trunk/mergebot/templates/mergebot.html
r16 r17 1 <?cs include "header.cs"?> 2 <?cs include "macros.cs"?> 1 <!DOCTYPE html 2 PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" 3 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> 4 <html xmlns="http://www.w3.org/1999/xhtml" 5 xmlns:py="http://genshi.edgewall.org/" 6 xmlns:xi="http://www.w3.org/2001/XInclude"> 7 <xi:include href="layout.html" /> 8 <xi:include href="macros.html" /> 3 9 4 <div id="ctxtnav"></div> 10 <head> 11 <title> 12 MergeBot 13 </title> 14 </head> 5 15 6 <div id="content" class="mergebot"> 16 <body> 17 <div id="content" class="mergebot"> 7 18 8 <h1>MergeBot</h1> 19 <div id="content" class="query"> 20 <h1>Queued</h1> 21 <table class="listing tickets"> 22 <thead><tr> 23 <th>Ticket</th> 24 <th>Summary</th> 9 25 10 <!-- DEBUG SECTION --> 11 <?cs if:mergebot.debug ?> 12 <h2>Debug info:</h2> 13 <?cs each:debug = mergebot.debug ?> 14 <li><?cs var:debug ?></li> 15 <?cs /each ?> 16 <hr> 17 <?cs /if ?> 18 <!-- END DEBUG SECTION --> 26 <th>Requestor</th> 27 <th>Action</th> 28 <th>Task ID</th> 29 <th>Task Status</th> 19 30 20 <!-- Show all of our queues -->21 <?cs each:queue = mergebot.queue ?>22 <div id="content" class="query">23 <h1><?cs var:queue ?> Queue</h1>31 <th>Component</th> 32 <th>Version</th> 33 <th>Actions</th> 34 </tr></thead> 24 35 25 <table class="listing tickets"> 26 <!-- Hardcode the header for now: --> 27 <thead><tr> 28 <th>Ticket</th> 29 <th>Summary</th> 30 <th>Component</th> 31 <th>Version</th> 32 <th>Status</th> 33 <th>Requestor</th> 34 <th>Activity</th> <!-- for lack of a better name --> 35 </tr></thead> 36 <tr py:for="task_id, task_status, ticket_id, action, component, version, requestor, summary in queue"> 37 <td><a href="${href.ticket(ticket_id)}">#${ticket_id}</a></td> 38 <td><a href="${href.ticket(ticket_id)}">${summary}</a></td> 36 39 37 <?cs if:queue.current ?> 38 <tr> 39 <th><a href=<?cs var:queue.current.href ?>>#<?cs var:queue.current ?></a></th> <!-- #ticket --> 40 <th><a href=<?cs var:queue.current.href ?>><?cs var:queue.current.summary ?></a></th> 41 <th><?cs var:queue.current.component ?></th> 42 <th><?cs var:queue.current.version ?></th> 43 <th><?cs var:queue.current.status ?></th> 44 <th><?cs var:queue.current.requestor ?></th> 45 <th>Doing <?cs name:queue ?></th> <!-- activity --> 46 </tr> 47 <?cs /if ?> 48 <?cs each:task = queue.queue ?> <!-- was queue.queue --> 49 <tr> 50 <th><a href=<?cs var:task.href ?>>#<?cs var:task ?></a></th> <!-- #ticket --> 51 <th><a href=<?cs var:task.href ?>><?cs var:task.summary ?></a></th> 52 <th><?cs var:task.component ?></th> 53 <th><?cs var:task.version ?></th> 54 <th><?cs var:task.status ?></th> 55 <th><?cs var:task.requestor ?></th> 56 <th><?cs var:task.mergebotstate ?></th> <!-- activity --> 57 </tr> 58 <?cs /each ?> 40 <td>${requestor}</td> 41 <td>${action}</td> 42 <td>${task_id}</td> 43 <td>${task_status}</td> 59 44 60 </table> 61 </div> 62 <?cs /each ?> 45 <td>${component}</td> 46 <td>${version}</td> 47 <td> 48 <form id="cancel_tasks" method="post" name="cancel-task-%{task_id}" action=""> 49 <input type="hidden" name="task" value="${task_id}"/> 50 <input type="submit" name="action" value="Cancel" py:if="task_status in ['Waiting', 'Pending']"/> 51 </form> 52 </td> 53 </tr> 54 </table> 55 </div> 63 56 64 <!-- Tickets that are not in an activity queue: -->65 <div id="content" class="query">66 <h1>Unqueued</h1>57 <!-- Tickets that are not in an activity queue: --> 58 <div id="content" class="query"> 59 <h1>Unqueued</h1> 67 60 68 <table class="listing tickets"> 69 <!-- Hardcode the header for now: --> 70 <thead><tr> 71 <th>Ticket</th> 72 <th>Summary</th> 73 <th>Component</th> 74 <th>Version</th> 75 <th>Status</th> 76 <th>MergeBotState</th> 77 <th>Actions</th> 78 </tr></thead> 61 <table class="listing tickets"> 62 <thead><tr> 63 <th>Ticket</th> 64 <th>Summary</th> 65 <th>Component</th> 66 <th>Version</th> 67 <th>Status</th> 68 <th>MergeBotState</th> 69 <th>Actions</th> 70 </tr></thead> 71 <tr py:for="ticket in unqueued"> 72 <td><a href="${href.ticket(ticket.info.id)}">${ticket.info.id}</a></td> 73 <td><a href="${href.ticket(ticket.info.id)}">${ticket.info.summary}</a></td> 74 <td>${ticket.info.component}</td> 75 <td>${ticket.info.version}</td> 76 <td>${ticket.info.status}</td> 77 <td>${ticket.info.mergebotstate}</td> 78 <td> 79 <form id="ops" method="post" name="ops-${ticket.info.id}" action=""> 80 <input type="hidden" name="ticket" value="${ticket.info.id}" /> 81 <input type="hidden" name="component" value="${ticket.info.component}" /> 82 <input type="hidden" name="version" value="${ticket.info.version}" /> 83 <input type="submit" name="action" value="Branch" py:if="ticket.branch"/> 84 <input type="submit" name="action" value="Rebranch" py:if="ticket.rebranch"/> 85 <input type="submit" name="action" value="Merge" py:if="ticket.merge"/> 86 <input type="submit" name="action" value="CheckMerge" py:if="ticket.checkmerge"/> 87 </form> 88 </td> 89 </tr> 90 </table> 91 </div> 79 92 80 <?cs each:task = mergebot.notqueued ?> 81 <tr> 82 <th><a href=<?cs var:task.href ?>>#<?cs var:task ?></a></th> <!-- ticket --> 83 <th><a href=<?cs var:task.href ?>><?cs var:task.summary ?></a></th> <!-- summary --> 84 <th><?cs var:task.component ?></th> 85 <th><?cs var:task.version ?></th> 86 <th><?cs var:task.status ?></th> 87 <th><?cs var:task.mergebotstate ?></th> 88 89 <td> 90 <!-- For each of the buttons, MergeBot's web_ui tells us if it's a 91 valid operation for the ticket. Use that to determine which buttons to 92 show. --> 93 <form id="ops" method="post" name="ops-<?cs var:task ?>" action=""> 94 <input type="hidden" name="ticket" value="<?cs var:task ?>" /> 95 <input type="hidden" name="component" value="<?cs var:task.component ?>" /> 96 <input type="hidden" name="version" value="<?cs var:task.version ?>" /> 97 <?cs if:task.actions.branch ?> 98 <input type="submit" name="action" value="Branch" /> 99 <?cs /if ?> 100 <?cs if:task.actions.rebranch ?> 101 <input type="submit" name="action" value="Rebranch" /> 102 <?cs /if ?> 103 <?cs if:task.actions.merge ?> 104 <input type="submit" name="action" value="Merge" /> 105 <?cs /if ?> 106 <?cs if:task.actions.checkmerge ?> 107 <input type="submit" name="action" value="CheckMerge" /> 108 <?cs /if ?> 109 </form> 110 </td> 111 112 </tr> 113 <?cs /each ?> 114 115 </table> 116 </div> 117 118 119 </div> 120 121 <?cs include:"footer.cs"?> 93 </div> 94 </body> 95 </html> -
mergebot/trunk/mergebot/web_ui.py
r16 r17 1 1 #!/usr/bin/env python 2 2 3 import random 3 import socket 4 import time 5 import os 6 import base64 7 from subprocess import check_call 4 8 5 9 from trac.core import * … … 7 11 from trac.ticket.query import Query 8 12 from trac.ticket.model import Ticket 9 from trac. config import Option13 from trac.env import IEnvironmentSetupParticipant 10 14 from trac.perm import IPermissionRequestor 11 15 from trac.web.main import IRequestHandler 12 from trac.web.chrome import INavigationContributor, ITemplateProvider 13 14 from MergeActor import MergeActor 15 from BranchActor import BranchActor 16 from RebranchActor import RebranchActor 17 from CheckMergeActor import CheckMergeActor 16 from trac.web.chrome import INavigationContributor, ITemplateProvider, add_warning 17 18 from action_logic import is_branchable, is_rebranchable, is_mergeable, is_checkmergeable 18 19 19 20 class MergeBotModule(Component): 20 implements(INavigationContributor, IPermissionRequestor, IRequestHandler, ITemplateProvider) 21 implements(INavigationContributor, IPermissionRequestor, IRequestHandler, ITemplateProvider, IEnvironmentSetupParticipant) 22 23 # IEnvironmentSetupParticipant 24 def environment_created(self): 25 self._setup_config() 26 27 def environment_needs_upgrade(self, db): 28 return not list(self.config.options('mergebot')) 29 30 def upgrade_environment(self, db): 31 self._setup_config() 32 33 def _setup_config(self): 34 self.config.set('mergebot', 'work_dir', 'mergebot') 35 self.config.set('mergebot', 'repository_url', 'http://FIXME/svn') 36 self.config.set('mergebot', 'listen.ip', 'localhost') 37 self.config.set('mergebot', 'listen.port', '12345') 38 self.config.set('mergebot', 'worker_count', '2') 39 # Set up the needed custom field for the bot 40 self.config.set('ticket-custom', 'mergebotstate', 'select') 41 self.config.set('ticket-custom', 'mergebotstate.label', 'MergeBotState') 42 self.config.set('ticket-custom', 'mergebotstate.options', '| merged | branched | conflicts') 43 self.config.set('ticket-custom', 'mergebotstate.value', '') 44 self.config.save() 21 45 22 46 # INavigationContributor … … 24 48 def get_active_navigation_item(self, req): 25 49 return 'mergebot' 50 26 51 def get_navigation_items(self, req): 27 52 """Generator that yields the MergeBot tab, but only if the user has … … 57 82 fields = ['summary', 'component', 'version', 'status'] 58 83 info = {} 59 ticket = Ticket(self.env, ticketid) 60 for field in fields: 61 info[field] = ticket[field] 62 info['href'] = self.env.href.ticket(ticketid) 63 self.log.debug("id=%s, info=%r" % (ticketid, info)) 84 if ticketid: 85 ticket = Ticket(self.env, ticketid) 86 for field in fields: 87 info[field] = ticket[field] 88 info['href'] = self.env.href.ticket(ticketid) 89 self.log.debug("id=%s, info=%r" % (ticketid, info)) 64 90 return info 91 92 def daemon_address(self): 93 host = self.env.config.get('mergebot', 'listen.ip') 94 port = int(self.env.config.get('mergebot', 'listen.port')) 95 return (host, port) 96 97 def start_daemon(self): 98 check_call(['mergebotdaemon', self.env.path]) 99 time.sleep(1) # bleh 100 101 def _daemon_cmd(self, cmd): 102 self.log.debug('Sending mergebotdaemon: %r' % cmd) 103 try: 104 info_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 105 info_socket.connect(self.daemon_address()) 106 except socket.error, e: 107 # if we're refused, try starting the daemon and re-try 108 if e and e[0] == 111: 109 self.log.debug('connection to mergebotdaemon refused, trying to start mergebotdaemon') 110 self.start_daemon() 111 self.log.debug('Resending mergebotdaemon: %r' % cmd) 112 info_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 113 info_socket.connect(self.daemon_address()) 114 info_socket.sendall(cmd) 115 self.log.debug('Reading mergebotdaemon response') 116 raw = info_socket.recv(4096) 117 info_socket.close() 118 self.log.debug('Reading mergebotdaemon response was %r' % raw) 119 return raw 65 120 66 121 def process_request(self, req): … … 68 123 req.perm.assert_permission("MERGEBOT_VIEW") 69 124 70 # 2nd redirect back to the real mergebot page To address POST 125 # 2nd redirect back to the real mergebot page to address POST and 126 # browser refresh 71 127 if req.path_info == "/mergebot/redir": 72 128 req.redirect(req.href.mergebot()) 73 129 74 debugs = [] 75 req.hdf["title"] = "MergeBot" 130 data = {} 76 131 77 132 if req.method == "POST": # The user hit a button 78 #debugs += [79 # "POST",80 # "Branching ticket %s" % req.args,81 #]82 83 ticketnum = req.args['ticket']84 component = req.args['component']85 version = req.args['version']86 requestor = req.authname or "anonymous"87 ticket = Ticket(self.env, int(ticketnum))88 # FIXME: check for 'todo' key?89 # If the request is not valid, just ignore it.90 actor = None91 if req.args['action'] == "Branch":92 req.perm.assert_permission("MERGEBOT_BRANCH")93 if self._is_branchable(ticket):94 actor = BranchActor(self.env)95 elif req.args['action'] == "Rebranch":96 req.perm.assert_permission("MERGEBOT_BRANCH")97 if self._is_rebranchable(ticket):98 actor = RebranchActor(self.env)99 elif req.args['action'] == "CheckMerge":100 req.perm.assert_permission("MERGEBOT_VIEW")101 if self._is_checkmergeable(ticket):102 actor = CheckMergeActor(self.env)103 elif req.args['action'] == "Merge":104 if version.startswith("#"):105 req.perm.assert_permission("MERGEBOT_MERGE_TICKET")106 else:107 req.perm.assert_permission("MERGEBOT_MERGE_RELEASE")108 if self._is_mergeable(ticket):109 actor = MergeActor(self.env)110 if actor:111 actor.AddTask([ticketnum, component, version, requestor])112 try:113 actor.Run() # Starts processing deamon.114 except Exception, e:115 self.log.exception(e)133 if req.args['action'] in ['Branch', 'Rebranch', 'CheckMerge', 'Merge']: 134 ticketnum = req.args['ticket'] 135 component = req.args['component'] 136 version = req.args['version'] 137 requestor = req.authname or 'anonymous' 138 ticket = Ticket(self.env, int(ticketnum)) 139 # FIXME: check for 'todo' key? 140 # If the request is not valid, just ignore it. 141 action = None 142 if req.args['action'] == "Branch": 143 req.perm.assert_permission("MERGEBOT_BRANCH") 144 if is_branchable(ticket): 145 action = 'branch' 146 elif req.args['action'] == "Rebranch": 147 req.perm.assert_permission("MERGEBOT_BRANCH") 148 if is_rebranchable(ticket): 149 action = 'rebranch' 150 elif req.args['action'] == "CheckMerge": 151 req.perm.assert_permission("MERGEBOT_VIEW") 152 if is_checkmergeable(ticket): 153 action = 'checkmerge' 154 elif req.args['action'] == "Merge": 155 if version.startswith("#"): 156 req.perm.assert_permission("MERGEBOT_MERGE_TICKET") 157 else: 158 req.perm.assert_permission("MERGEBOT_MERGE_RELEASE") 159 if is_mergeable(ticket): 160 action = 'merge' 161 if action: 162 command = 'ADD %s %s %s %s %s\nQUIT\n' % (ticketnum, action, component, version, requestor) 163 result = self._daemon_cmd(command) 164 if 'OK' not in result: 165 add_warning(req, result) 166 if req.args['action'] == "Cancel": 167 command = 'CANCEL %s\nQUIT\n' % req.args['task'] 168 result = self._daemon_cmd(command) 169 if 'OK' not in result: 170 add_warning(req, result) 116 171 # First half of a double-redirect to make a refresh not re-send the 117 172 # POST data. … … 120 175 # We want to fill out the information for the page unconditionally. 121 176 177 # Connect to the daemon and read the current queue information 178 raw_queue_info = self._daemon_cmd('LIST\nQUIT\n') 179 # Parse the queue information into something we can display 180 queue_info = [x.split(',') for x in raw_queue_info.split('\n') if ',' in x] 181 status_map = { 182 'Z':'Zombie', 183 'R':'Running', 184 'Q':'Queued', 185 'W':'Waiting', 186 'P':'Pending', 187 } 188 for row in queue_info: 189 status = row[1] 190 row[1] = status_map[status] 191 summary = row[7] 192 row[7] = base64.b64decode(summary) 193 194 data['queue'] = queue_info 195 196 queued_tickets = set([int(q[2]) for q in queue_info]) 197 198 # Provide the list of tickets at the bottom of the page, along with 199 # flags for which buttons should be enabled for each ticket. 122 200 # I need to get a list of tickets. For non-admins, restrict the list 123 201 # to the tickets owned by that user. 124 querystring = "status=new|assigned|reopened&version!=" 125 if not req.perm.has_permission("MERGEBOT_ADMIN"): 202 querystring = "status!=closed&version!=" 203 required_columns = ["component", "version", "mergebotstate"] 204 if req.perm.has_permission("MERGEBOT_ADMIN"): 205 required_columns.append("owner") 206 else: 126 207 querystring += "&owner=%s" % (req.authname,) 127 208 query = Query.from_string(self.env, querystring, order="id") 128 209 columns = query.get_columns() 129 for name in ("component", "version", "mergebotstate"):210 for name in required_columns: 130 211 if name not in columns: 131 212 columns.append(name) 132 #debugs.append("query.fields = %s" % str(query.fields))133 213 db = self.env.get_db_cnx() 134 214 tickets = query.execute(req, db) 135 #debugs += map(str, tickets) 136 req.hdf['mergebot.ticketcount'] = str(len(tickets)) 137 138 # Make the tickets indexable by ticket id number: 139 ticketinfo = {} 215 data['unqueued'] = [] 140 216 for ticket in tickets: 141 ticketinfo[ticket['id']] = ticket142 #debugs.append(str(ticketinfo))143 144 availableTickets = tickets[:]145 queued_tickets = []146 # We currently have 4 queues, "branch", "rebranch", "checkmerge", and147 # "merge"148 queues = [149 ("branch", BranchActor),150 ("rebranch", RebranchActor),151 ("checkmerge", CheckMergeActor),152 ("merge", MergeActor)153 ]154 for queuename, actor in queues:155 status, queue = actor(self.env).GetStatus()156 req.hdf["mergebot.queue.%s" % (queuename, )] = queuename157 if status:158 # status[0] is ticketnum159 req.hdf["mergebot.queue.%s.current" % (queuename)] = status[0]160 req.hdf["mergebot.queue.%s.current.requestor" % (queuename)] = status[3]161 ticketnum = int(status[0])162 queued_tickets.append(ticketnum)163 ticket = self._get_ticket_info(ticketnum)164 for field, value in ticket.items():165 req.hdf["mergebot.queue.%s.current.%s" % (queuename, field)] = value166 else:167 req.hdf["mergebot.queue.%s.current" % (queuename)] = ""168 req.hdf["mergebot.queue.%s.current.requestor" % (queuename)] = ""169 170 for i in range(len(queue)):171 ticketnum = int(queue[i][0])172 queued_tickets.append(ticketnum)173 req.hdf["mergebot.queue.%s.queue.%d" % (queuename, i)] = str(ticketnum)174 req.hdf["mergebot.queue.%s.queue.%d.requestor" % (queuename, i)] = queue[i][3]175 ticket = self._get_ticket_info(ticketnum)176 for field, value in ticket.items():177 req.hdf["mergebot.queue.%s.queue.%d.%s" % (queuename, i, field)] = value178 #debugs.append("%s queue %d, ticket #%d, %s = %s" % (queuename, i, ticketnum, field, value))179 180 # Provide the list of tickets at the bottom of the page, along with181 # flags for which buttons should be enabled for each ticket.182 for ticket in availableTickets:183 217 ticketnum = ticket['id'] 184 218 if ticketnum in queued_tickets: 185 219 # Don't allow more actions to be taken on a ticket that is 186 220 # currently queued for something. In the future, we may want 187 # to support a 'Cancel' button, though. 221 # to support a 'Cancel' button, though. Or present actions 222 # based upon the expected state of the ticket 188 223 continue 189 req.hdf["mergebot.notqueued.%d" % (ticketnum)] = str(ticketnum) 190 for field, value in ticket.items(): 191 req.hdf["mergebot.notqueued.%d.%s" % (ticketnum, field)] = value 224 225 ticket_info = {'info': ticket} 226 data['unqueued'].append(ticket_info) 227 192 228 # Select what actions this user may make on this ticket based on 193 229 # its current state. 230 194 231 # MERGE 195 req.hdf["mergebot.notqueued.%d.actions.merge" % (ticketnum)] = 0196 if req.perm.has_permission("MERGEBOT_MERGE_RELEASE") and \197 not ticket['version'].startswith("#"):198 req.hdf["mergebot.notqueued.%d.actions.merge" % (ticketnum)] = self._is_mergeable(ticket)199 if req.perm.has_permission("MERGEBOT_MERGE_TICKET") and \200 ticket['version'].startswith("#"):201 req.hdf["mergebot.notqueued.%d.actions.merge" % (ticketnum)] = self._is_mergeable(ticket)232 ticket_info['merge'] = False 233 if ticket['version'].startswith('#'): 234 if req.perm.has_permission("MERGEBOT_MERGE_TICKET"): 235 ticket_info['merge'] = is_mergeable(ticket) 236 else: 237 if req.perm.has_permission("MERGEBOT_MERGE_RELEASE"): 238 ticket_info['merge'] = is_mergeable(ticket) 202 239 # CHECK-MERGE 203 240 if req.perm.has_permission("MERGEBOT_VIEW"): 204 req.hdf["mergebot.notqueued.%d.actions.checkmerge" % (ticketnum)] = self._is_checkmergeable(ticket)241 ticket_info['checkmerge'] = is_checkmergeable(ticket) 205 242 else: 206 req.hdf["mergebot.notqueued.%d.actions.checkmerge" % (ticketnum)] = 0243 ticket_info['checkmerge'] = False 207 244 # BRANCH, REBRANCH 208 245 if req.perm.has_permission("MERGEBOT_BRANCH"): 209 req.hdf["mergebot.notqueued.%d.actions.branch" % (ticketnum)] = self._is_branchable(ticket)210 req.hdf["mergebot.notqueued.%d.actions.rebranch" % (ticketnum)] = self._is_rebranchable(ticket)246 ticket_info['branch'] = is_branchable(ticket) 247 ticket_info['rebranch'] = is_rebranchable(ticket) 211 248 else: 212 req.hdf["mergebot.notqueued.%d.actions.branch" % (ticketnum)] = 0 213 req.hdf["mergebot.notqueued.%d.actions.rebranch" % (ticketnum)] = 0 214 215 # Add debugs: 216 req.hdf["mergebot.debug"] = len(debugs) 217 for i in range(len(debugs)): 218 req.hdf["mergebot.debug.%d" % (i)] = debugs[i] 219 220 return "mergebot.cs", None 221 222 def _is_branchable(self, ticket): 223 try: 224 state = ticket['mergebotstate'] 225 except KeyError: 226 state = "" 227 return state == "" or state == "merged" 228 def _is_rebranchable(self, ticket): 229 # TODO: we should be able to tell if trunk (or version) has had commits 230 # since we branched, and only mark it as rebranchable if there have 231 # been. 232 try: 233 state = ticket['mergebotstate'] 234 except KeyError: 235 state = "" 236 return state in ["branched", "conflicts"] 237 def _is_mergeable(self, ticket): 238 try: 239 state = ticket['mergebotstate'] 240 except KeyError: 241 state = "" 242 return state == "branched" 243 def _is_checkmergeable(self, ticket): 244 try: 245 state = ticket['mergebotstate'] 246 except KeyError: 247 state = "" 248 return state == "branched" or state == "conflicts" 249 ticket_info['branch'] = False 250 ticket_info['rebranch'] = False 251 252 # proactive warnings 253 work_dir = self.env.config.get('mergebot', 'work_dir') 254 if not os.path.isabs(work_dir): 255 work_dir = os.path.join(self.env.path, work_dir) 256 if not os.path.isdir(work_dir): 257 add_warning(req, 'The Mergebot work directory "%s" does not exist.' % work_dir) 258 259 return "mergebot.html", data, None 249 260 250 261 # ITemplateProvider 251 262 def get_htdocs_dirs(self): 252 263 return [] 264 253 265 def get_templates_dirs(self): 254 266 # It appears that everyone does this import here instead of at the top -
mergebot/trunk/setup.py
r16 r17 5 5 setup( 6 6 name = "TracMergeBot", 7 version = "0. 9",7 version = "0.11", 8 8 author = "Eli Carter", 9 author_email = "eli.carter@ commprove.com",10 url = "http ://www.commprove.com",9 author_email = "eli.carter@retracile.net", 10 url = "https://retracile.net", 11 11 description = "Branch and merge management plugin", 12 12 license = """MIT""", 13 13 zip_safe=False, 14 14 packages=["mergebot"], 15 package_data={"mergebot": ["templates/*. cs"]},15 package_data={"mergebot": ["templates/*.html"]}, 16 16 17 17 install_requires = [], 18 18 19 19 entry_points = { 20 "console_scripts": [ 21 "mergebotdaemon = mergebot.mergebotdaemon:run", 22 ], 20 23 "trac.plugins": [ 21 "mergebot.web_ui = mergebot.web_ui" 24 "mergebot.web_ui = mergebot.web_ui", 25 "mergebot.ticket_actions = mergebot.ticket_actions" 22 26 ] 23 27 }, -
mergebot/trunk/utils/makerpm
r16 r17 1 1 #!/bin/bash 2 python setup.py bdist_rpm --install-script= bdist_rpm_installscript2 python setup.py bdist_rpm --install-script=utils/bdist_rpm_installscript -
mergebot/trunk/utils/test.py
r16 r17 12 12 13 13 14 from trac.tests.functional import FunctionalTestSuite, FunctionalTestEnvironment, FunctionalTester, TwillTest, tc, b, logfile14 from trac.tests.functional import FunctionalTestSuite, FunctionalTestEnvironment, FunctionalTester, FunctionalTwillTestCaseSetup, tc, b, logfile 15 15 from trac.tests.contentgen import random_page #, random_sentence, random_word 16 16 17 17 18 class MergeBotTestEnvironment(FunctionalTestEnvironment):19 """Slight change to FunctionalTestEnvironment to keep the PYTHONPATH from20 our environment.21 """22 def start(self):23 """Starts the webserver"""24 server = Popen(["python", "./trac/web/standalone.py",25 "--port=%s" % self.port, "-s",26 "--basic-auth=trac,%s," % self.htpasswd,27 self.tracdir],28 #env={'PYTHONPATH':'.'},29 stdout=logfile, stderr=logfile,30 )31 self.pid = server.pid32 time.sleep(1) # Give the server time to come up33 34 def _tracadmin(self, *args):35 """Internal utility method for calling trac-admin"""36 if call(["python", "./trac/admin/console.py", self.tracdir] +37 list(args),38 #env={'PYTHONPATH':'.'},39 stdout=logfile, stderr=logfile):40 raise Exception('Failed running trac-admin with %r' % (args, ))41 42 43 FunctionalTestEnvironment = MergeBotTestEnvironment18 #class MergeBotTestEnvironment(FunctionalTestEnvironment): 19 # """Slight change to FunctionalTestEnvironment to keep the PYTHONPATH from 20 # our environment. 21 # """ 22 # def start(self): 23 # """Starts the webserver""" 24 # server = Popen(["python", "./trac/web/standalone.py", 25 # "--port=%s" % self.port, "-s", 26 # "--basic-auth=trac,%s," % self.htpasswd, 27 # self.tracdir], 28 # #env={'PYTHONPATH':'.'}, 29 # stdout=logfile, stderr=logfile, 30 # ) 31 # self.pid = server.pid 32 # time.sleep(1) # Give the server time to come up 33 # 34 # def _tracadmin(self, *args): 35 # """Internal utility method for calling trac-admin""" 36 # if call(["python", "./trac/admin/console.py", self.tracdir] + 37 # list(args), 38 # #env={'PYTHONPATH':'.'}, 39 # stdout=logfile, stderr=logfile): 40 # raise Exception('Failed running trac-admin with %r' % (args, )) 41 # 42 # 43 #FunctionalTestEnvironment = MergeBotTestEnvironment 44 44 45 45 … … 148 148 port = 8889 149 149 baseurl = "http://localhost:%s" % port 150 self._testenv = FunctionalTestEnvironment("testenv%s" % port, port )150 self._testenv = FunctionalTestEnvironment("testenv%s" % port, port, baseurl) 151 151 152 152 # Configure mergebot … … 186 186 187 187 188 class MergeBotTestEnabled( TwillTest):188 class MergeBotTestEnabled(FunctionalTwillTestCaseSetup): 189 189 def runTest(self): 190 190 self._tester.logout() … … 197 197 198 198 199 class MergeBotTestQueueList( TwillTest):199 class MergeBotTestQueueList(FunctionalTwillTestCaseSetup): 200 200 def runTest(self): 201 201 tc.follow('MergeBot') … … 204 204 205 205 206 class MergeBotTestNoVersion( TwillTest):206 class MergeBotTestNoVersion(FunctionalTwillTestCaseSetup): 207 207 """Verify that if a ticket does not have the version field set, it will not 208 208 appear in the MergeBot list. … … 215 215 216 216 217 class MergeBotTestBranch( TwillTest):217 class MergeBotTestBranch(FunctionalTwillTestCaseSetup): 218 218 def runTest(self): 219 219 """Verify that the 'branch' button works""" … … 223 223 224 224 225 class MergeBotTestRebranch( TwillTest):225 class MergeBotTestRebranch(FunctionalTwillTestCaseSetup): 226 226 def runTest(self): 227 227 """Verify that the 'rebranch' button works""" … … 232 232 233 233 234 class MergeBotTestMerge( TwillTest):234 class MergeBotTestMerge(FunctionalTwillTestCaseSetup): 235 235 def runTest(self): 236 236 """Verify that the 'merge' button works""" … … 241 241 242 242 243 class MergeBotTestCheckMerge( TwillTest):243 class MergeBotTestCheckMerge(FunctionalTwillTestCaseSetup): 244 244 def runTest(self): 245 245 """Verify that the 'checkmerge' button works""" … … 250 250 251 251 252 class MergeBotTestSingleUseCase( TwillTest):252 class MergeBotTestSingleUseCase(FunctionalTwillTestCaseSetup): 253 253 def runTest(self): 254 254 """Create a branch, make a change, checkmerge, and merge it."""
Note: See TracChangeset
for help on using the changeset viewer.