Changeset 17


Ignore:
Timestamp:
Jun 8, 2009 3:07:47 AM (16 years ago)
Author:
retracile
Message:

Mergebot: redesigned implementation. Still has rough edges.

Location:
mergebot/trunk
Files:
7 added
2 deleted
8 edited
4 moved

Legend:

Unmodified
Added
Removed
  • mergebot/trunk/COPYING

    r16 r17  
    22
    33Copyright (c) 2007 CommProve, Inc.
     4Copyright (c) 2008-2009 Eli Carter <eli.carter@retracile.net>
    45
    56Permission is hereby granted, free of charge, to any person obtaining a copy
  • mergebot/trunk/README.txt

    r16 r17  
    11= Installing =
    22# Checkout the source.
    3 $ svn co <url> mergebot-0.10
    4 $ cd mergebot-0.10
    5 $ python setup.py bdist_egg
    6 $ 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"
    77# Create the mergebot work area
    88$ mkdir <mytracenv>/mergebot
    99$ chown <webserver>:<webserver> <mytracenv>/mergebot
    1010
    11 # Add to trac.ini:
    12 [mergebot]
    13 work_dir = /var/www/trac/mytracenv/mergebot
    14 repository_url = http://servername/repos
     11Enable mergebot in the [components] section:
     12mergebot.web_ui.* = enabled
    1513
    16 [ticket-custom]
    17 mergebotstate = select
    18 mergebotstate.label = MergeBotState
    19 mergebotstate.options = | tomerge | merged | tobranch | branched | conflicts
    20 mergebotstate.value =
     14If you want to use the ticket workflow features you will also need to change
     15[components]
     16mergebot.ticket_actions.* = enabled
     17[ticket]
     18workflow = ConfigurableTicketWorkflow,MergebotActionController
     19Restart your webserver.
     20And then use "mergebot" in the .operations section of the relevant ticket actions.
    2121
    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
     24Then edit <mytracenv>/conf/trac.ini again.
     25In particular, look for the [mergebot] section that the upgrade added, and be sure the repository_url is set.
     26Give MERGEBOT_* permissions to your users.
    2827
    29 # Restart your webserver
     28Be sure you have a version named 'trunk' -- and you will likely want to make that the default.
     29Mergebot 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  
    11#!/usr/bin/env python
    22"""Module for creating new branches for tickets"""
    3 # Syntax: BranchActor.py ticketnum component version requestor
    43
    54import os
    6 import sys
    75import time
    8 import trac.env
    96
    10 import SvnOps
    11 from WorkQueue import MergeBotActor, VersionToDir
    12 from TrackerTools import GetRepositoryPublicUrl, GetRepositoryLocalUrl, Task, \
    13     GetLogFile
     7from mergebot import SvnOps
     8from mergebot.Actor import Actor
    149
    15 def branch_action(trac_env, ticketnum, component, version, requestor):
    16     "Create the branch"
    17     task_obj = Task(trac_env, ticketnum)
     10class 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))
    1820
    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
    2332
    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)
    2934
    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
    3148
    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
    3650        commit_message = "\n".join([commit_header,
    37             "    Delete old branch",
     51            "    Create branch from %s for %s." % (self.version,
     52                                                   self.requestor),
    3853        ])
    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            '}}}',
    6673        ])
    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
    8575
    8676# vim:foldcolumn=4 foldmethod=indent
  • mergebot/trunk/mergebot/CheckMergeActor.py

    r16 r17  
    11#!/usr/bin/env python
    22"""
    3 Syntax: MergeActor.py ticketnum component version requestor
    4 
    53Verify that a branch can be merged to its trunk without conflicts, but don't
    64commit the merge.
     
    86
    97import os
    10 import sys
    11 import trac.env
     8import shutil
    129
    13 import SvnOps
    14 from WorkQueue import MergeBotActor, VersionToDir
    15 from TrackerTools import GetWorkDir, GetRepositoryLocalUrl, Task, GetLogFile
     10from mergebot import SvnOps
     11from mergebot.Actor import Actor
    1612
    17 def check_merge_action(trac_env, ticketnum, component, version, requestor):
     13class CheckMergeActor(Actor):
     14    """Checks that this ticket can be merged to its baseline, but don't modify
     15    the repository.
    1816    """
    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()
    2925
    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)
    3428
    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
    3842
    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
    5949
    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
    6271
    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)
    6575
    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
    8077
    8178# vim:foldcolumn=4 foldmethod=indent
  • mergebot/trunk/mergebot/MergeActor.py

    r16 r17  
    77
    88import os
    9 import sys
    10 import trac.env
     9import shutil
    1110
    12 import SvnOps
    13 from WorkQueue import MergeBotActor, VersionToDir
    14 from TrackerTools import GetWorkDir, GetRepositoryLocalUrl, Task, GetLogFile
     11from mergebot import SvnOps
     12from mergebot.Actor import Actor
    1513
    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)
     14class 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)
    2026
    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
    2538
    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
    3044
    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
    3398
    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)
    81102
    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
    102104
    103105# vim:foldcolumn=4 foldmethod=indent
  • mergebot/trunk/mergebot/RebranchActor.py

    r16 r17  
    11#!/usr/bin/env python
    22"""
    3 Syntax: RebranchActor.py ticketnum component version requestor
    4 
    53Rebranch a branch from its trunk, pulling in the changes made on the branch if
    64possible.
    75"""
    86
     7import shutil
     8import time
    99import os
    10 import sys
    11 import time
    12 import trac.env
    1310
    14 import SvnOps
    15 from WorkQueue import MergeBotActor, VersionToDir
    16 from TrackerTools import GetRepositoryPublicUrl, GetRepositoryLocalUrl, Task, \
    17     GetWorkDir, GetLogFile
     11from mergebot import SvnOps
     12from mergebot.Actor import Actor
    1813
    19 def rebranch_action(trac_env, ticketnum, component, version, requestor):
     14class RebranchActor(Actor):
     15    """Rebranches a ticket from its baseline.
    2016    """
    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 deleted branch.
    25         if there are no conflicts, commit those changes to the 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 as
    29     # little cleanup as possible and the fewest cases where we leave a broken
    30     # 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.
    3329
    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 = {}
    3831
    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))
    4636
    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
    5450
    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
    5658
    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)
    10262
    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:",
    14571                "{{{",
    14672                publicsvnmergecommand,
    14773                "}}}",
     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,
    148142            ])
    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                ])
    157171
    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
    176175
    177176# 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" />
    39
    4 <div id="ctxtnav"></div>
     10  <head>
     11    <title>
     12        MergeBot
     13    </title>
     14  </head>
    515
    6 <div id="content" class="mergebot">
     16  <body>
     17  <div id="content" class="mergebot">
    718
    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>
    925
    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>
    1930
    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>
    2435
    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>
    3639
    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>
    5944
    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>
    6356
    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>
    6760
    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>
    7992
    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  
    11#!/usr/bin/env python
    22
    3 import random
     3import socket
     4import time
     5import os
     6import base64
     7from subprocess import check_call
    48
    59from trac.core import *
     
    711from trac.ticket.query import Query
    812from trac.ticket.model import Ticket
    9 from trac.config import Option
     13from trac.env import IEnvironmentSetupParticipant
    1014from trac.perm import IPermissionRequestor
    1115from 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
     16from trac.web.chrome import INavigationContributor, ITemplateProvider, add_warning
     17
     18from action_logic import is_branchable, is_rebranchable, is_mergeable, is_checkmergeable
    1819
    1920class 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()
    2145
    2246    # INavigationContributor
     
    2448    def get_active_navigation_item(self, req):
    2549        return 'mergebot'
     50
    2651    def get_navigation_items(self, req):
    2752        """Generator that yields the MergeBot tab, but only if the user has
     
    5782        fields = ['summary', 'component', 'version', 'status']
    5883        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))
    6490        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
    65120
    66121    def process_request(self, req):
     
    68123        req.perm.assert_permission("MERGEBOT_VIEW")
    69124
    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
    71127        if req.path_info == "/mergebot/redir":
    72128            req.redirect(req.href.mergebot())
    73129
    74         debugs = []
    75         req.hdf["title"] = "MergeBot"
     130        data = {}
    76131
    77132        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 = None
    91             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)
    116171            # First half of a double-redirect to make a refresh not re-send the
    117172            # POST data.
     
    120175        # We want to fill out the information for the page unconditionally.
    121176
     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.
    122200        # I need to get a list of tickets.  For non-admins, restrict the list
    123201        # 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:
    126207            querystring += "&owner=%s" % (req.authname,)
    127208        query = Query.from_string(self.env, querystring, order="id")
    128209        columns = query.get_columns()
    129         for name in ("component", "version", "mergebotstate"):
     210        for name in required_columns:
    130211            if name not in columns:
    131212                columns.append(name)
    132         #debugs.append("query.fields = %s" % str(query.fields))
    133213        db = self.env.get_db_cnx()
    134214        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'] = []
    140216        for ticket in tickets:
    141             ticketinfo[ticket['id']] = ticket
    142         #debugs.append(str(ticketinfo))
    143 
    144         availableTickets = tickets[:]
    145         queued_tickets = []
    146         # We currently have 4 queues, "branch", "rebranch", "checkmerge", and
    147         # "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, )] = queuename
    157             if status:
    158                 # status[0] is ticketnum
    159                 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)] = value
    166             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)] = value
    178                     #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 with
    181         # flags for which buttons should be enabled for each ticket.
    182         for ticket in availableTickets:
    183217            ticketnum = ticket['id']
    184218            if ticketnum in queued_tickets:
    185219                # Don't allow more actions to be taken on a ticket that is
    186220                # 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
    188223                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
    192228            # Select what actions this user may make on this ticket based on
    193229            # its current state.
     230
    194231            # MERGE
    195             req.hdf["mergebot.notqueued.%d.actions.merge" % (ticketnum)] = 0
    196             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)
    202239            # CHECK-MERGE
    203240            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)
    205242            else:
    206                 req.hdf["mergebot.notqueued.%d.actions.checkmerge" % (ticketnum)] = 0
     243                ticket_info['checkmerge'] = False
    207244            # BRANCH, REBRANCH
    208245            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)
    211248            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
    249260
    250261    # ITemplateProvider
    251262    def get_htdocs_dirs(self):
    252263        return []
     264
    253265    def get_templates_dirs(self):
    254266        # It appears that everyone does this import here instead of at the top
  • mergebot/trunk/setup.py

    r16 r17  
    55setup(
    66    name = "TracMergeBot",
    7     version = "0.9",
     7    version = "0.11",
    88    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",
    1111    description = "Branch and merge management plugin",
    1212    license = """MIT""",
    1313    zip_safe=False,
    1414    packages=["mergebot"],
    15     package_data={"mergebot": ["templates/*.cs"]},
     15    package_data={"mergebot": ["templates/*.html"]},
    1616
    1717    install_requires = [],
    1818
    1919    entry_points = {
     20        "console_scripts": [
     21            "mergebotdaemon = mergebot.mergebotdaemon:run",
     22        ],
    2023        "trac.plugins": [
    21             "mergebot.web_ui = mergebot.web_ui"
     24            "mergebot.web_ui = mergebot.web_ui",
     25            "mergebot.ticket_actions = mergebot.ticket_actions"
    2226        ]
    2327    },
  • mergebot/trunk/utils/makerpm

    r16 r17  
    11#!/bin/bash
    2 python setup.py bdist_rpm --install-script=bdist_rpm_installscript
     2python setup.py bdist_rpm --install-script=utils/bdist_rpm_installscript
  • mergebot/trunk/utils/test.py

    r16 r17  
    1212
    1313
    14 from trac.tests.functional import FunctionalTestSuite, FunctionalTestEnvironment, FunctionalTester, TwillTest, tc, b, logfile
     14from trac.tests.functional import FunctionalTestSuite, FunctionalTestEnvironment, FunctionalTester, FunctionalTwillTestCaseSetup, tc, b, logfile
    1515from trac.tests.contentgen import random_page #, random_sentence, random_word
    1616
    1717
    18 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
     18#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
    4444
    4545
     
    148148        port = 8889
    149149        baseurl = "http://localhost:%s" % port
    150         self._testenv = FunctionalTestEnvironment("testenv%s" % port, port)
     150        self._testenv = FunctionalTestEnvironment("testenv%s" % port, port, baseurl)
    151151
    152152        # Configure mergebot
     
    186186
    187187
    188 class MergeBotTestEnabled(TwillTest):
     188class MergeBotTestEnabled(FunctionalTwillTestCaseSetup):
    189189    def runTest(self):
    190190        self._tester.logout()
     
    197197
    198198
    199 class MergeBotTestQueueList(TwillTest):
     199class MergeBotTestQueueList(FunctionalTwillTestCaseSetup):
    200200    def runTest(self):
    201201        tc.follow('MergeBot')
     
    204204
    205205
    206 class MergeBotTestNoVersion(TwillTest):
     206class MergeBotTestNoVersion(FunctionalTwillTestCaseSetup):
    207207    """Verify that if a ticket does not have the version field set, it will not
    208208    appear in the MergeBot list.
     
    215215
    216216
    217 class MergeBotTestBranch(TwillTest):
     217class MergeBotTestBranch(FunctionalTwillTestCaseSetup):
    218218    def runTest(self):
    219219        """Verify that the 'branch' button works"""
     
    223223
    224224
    225 class MergeBotTestRebranch(TwillTest):
     225class MergeBotTestRebranch(FunctionalTwillTestCaseSetup):
    226226    def runTest(self):
    227227        """Verify that the 'rebranch' button works"""
     
    232232
    233233
    234 class MergeBotTestMerge(TwillTest):
     234class MergeBotTestMerge(FunctionalTwillTestCaseSetup):
    235235    def runTest(self):
    236236        """Verify that the 'merge' button works"""
     
    241241
    242242
    243 class MergeBotTestCheckMerge(TwillTest):
     243class MergeBotTestCheckMerge(FunctionalTwillTestCaseSetup):
    244244    def runTest(self):
    245245        """Verify that the 'checkmerge' button works"""
     
    250250
    251251
    252 class MergeBotTestSingleUseCase(TwillTest):
     252class MergeBotTestSingleUseCase(FunctionalTwillTestCaseSetup):
    253253    def runTest(self):
    254254        """Create a branch, make a change, checkmerge, and merge it."""
Note: See TracChangeset for help on using the changeset viewer.