diff -Naur trac-0.8.1-orig/templates/macros.cs trac-0.8.1/templates/macros.cs
--- trac-0.8.1-orig/templates/macros.cs	2005-01-24 15:48:41.000000000 +0100
+++ trac-0.8.1/templates/macros.cs	2005-03-14 16:27:50.888146890 +0100
@@ -10,6 +10,19 @@
  </select><?cs
 /def?>
 
+<?cs def:hdf_select_with_values(options, name, selected) ?>
+ <select size="1" id="<?cs var:name ?>" name="<?cs var:name ?>"><?cs
+  each:option = options ?><?cs
+   if option.name == $selected ?>
+    <option value="<?cs var:option.value ?>" selected="selected">
+    <?cs var:option.name ?></option><?cs
+   else ?>
+    <option value="<?cs var:option.value ?>"><?cs var:option.name ?></option><?cs
+   /if ?><?cs
+  /each ?>
+ </select><?cs
+/def?>
+
 <?cs def:hdf_select_multiple(options, name, size) ?>
  <select size="<?cs var:size ?>" id="<?cs var:name ?>" name="<?cs
    var:name ?>" multiple="multiple"><?cs
diff -Naur trac-0.8.1-orig/templates/ticket.cs trac-0.8.1/templates/ticket.cs
--- trac-0.8.1-orig/templates/ticket.cs	2004-12-20 11:09:29.000000000 +0100
+++ trac-0.8.1/templates/ticket.cs	2005-03-14 16:49:15.105208338 +0100
@@ -29,6 +29,19 @@
    /if ?></td><?cs if numprops % #2 && !$last_prop || fullrow ?>
  </tr><tr><?cs /if ?><?cs set numprops = $numprops + #1 - fullrow ?><?cs
  /def ?>
+ <?cs def:ticketrel(rel) ?>
+ <tr>
+ <form id="removerelation" method="post" action="<?cs var:cgi_location?>/ticket/<?cs
+  var:ticket.id ?>">
+  <input type="hidden" name="rel_from" value="<?cs var:rel.from_id ?>" />
+  <input type="hidden" name="rel_type" value="<?cs var:rel.type ?>" />
+  <input type="hidden" name="rel_to" value="<?cs var:rel.to_id ?>" />
+  <td colspan="2">
+    <div title="<?cs var:rel.description ?>"><?cs var:rel.relation ?></div>
+  <td><input type="submit" name="removerel_submit" value="Remove" /></td>
+ </form>
+ </tr><?cs
+ /def ?>
 
 <div id="ticket">
  <div class="date"><?cs var:ticket.opened ?></div>
@@ -66,6 +79,25 @@
   /each ?>
  </tr></table><?cs /if ?>
  <hr />
+ <table>
+ <tr><th colspan="3">Ticket relations</th></tr>
+ <?cs each:rel = ticket.relations ?>
+     <?cs call:ticketrel(rel) ?>
+ <?cs /each ?>
+ <?cs if trac.acl.TICKET_MODIFY ?>
+  <form id="newrelation" method="post" action="<?cs var:cgi_location?>/ticket/<?cs
+    var:ticket.id ?>">
+  <tr>
+  <td align="center"><?cs call:hdf_select_with_values(ticket.all_tickets, "rel_from", "This ticket") ?></td>
+  <td align="center"><?cs call:hdf_select(ticket.valid_relations, "rel_type", 0) ?><br />
+  <input type="submit" name="newrel_submit" value="Add Relation" /></td>
+  <td align="center"><?cs call:hdf_select_with_values(ticket.all_tickets, "rel_to", "this ticket") ?></td>
+  </tr>
+ </form>
+ <?cs /if ?>
+ </table>
+ 
+ <hr />
  <h3>Description<?cs if:ticket.reporter ?> by <?cs
    var:ticket.reporter ?><?cs /if ?>:</h3>
  <div class="description">
@@ -131,10 +163,10 @@
 </div><?cs /if ?>
 
 <?cs if $trac.acl.TICKET_MODIFY ?>
-<form action="<?cs var:cgi_location ?>#preview" method="post">
  <hr />
  <h3><a name="edit" onfocus="document.getElementById('comment').focus()">Add/Change #<?cs
    var:ticket.id ?> (<?cs var:ticket.summary ?>)</a></h3>
+<form action="<?cs var:cgi_location ?>#preview" method="post">
  <div class="field">
   <input type="hidden" name="mode" value="ticket" />
   <input type="hidden" name="id"   value="<?cs var:ticket.id ?>" />
diff -Naur trac-0.8.1-orig/trac/Ticket.py trac-0.8.1/trac/Ticket.py
--- trac-0.8.1-orig/trac/Ticket.py	2005-01-24 13:26:10.000000000 +0100
+++ trac-0.8.1/trac/Ticket.py	2005-03-14 16:51:25.219357101 +0100
@@ -26,6 +26,8 @@
 from UserDict import UserDict
 
 import perm
+import relation_hooks
+import sqlite
 import util
 from Module import Module
 from WikiFormatter import wiki_to_html
@@ -42,6 +44,7 @@
     def __init__(self, *args):
         UserDict.__init__(self)
         self._old = {}
+        self.relations = []
         if len(args) == 2:
             self._fetch_ticket(*args)
 
@@ -78,6 +81,17 @@
         if rows:
             for r in rows:
                 self['custom_' + r[0]] = r[1]
+        cursor.close()
+
+        cursor = db.cursor()
+        cursor.execute("SELECT * FROM xref WHERE source='ticket:%i' OR target='ticket:%i' ORDER BY context, source", id, id)
+        rows = cursor.fetchall()
+        if rows:
+            for r in rows:
+                self.relations.append({'from':r['source'], 'type':r['context'],
+                        'to':r['target']})
+        cursor.close()
+
         self._forget_changes()
 
     def populate(self, dict):
@@ -266,6 +280,75 @@
             hdf.setValue('%s.height' % pfx, f['height'])
         i += 1
 
+def insert_relation_fields(env, db, hdf, relations, id):
+    i = 0
+    for rel in relations:
+        name = 'ticket.relations.%i' % i
+        rel_string = ''
+        if rel['from'] == id:
+            rel_string += 'This ticket '
+        else:
+            rel_string += '%s ' % rel['from']
+        rel_string += rel['type']
+        if rel['to'] == id:
+            rel_string += ' this ticket.'
+        else:
+            rel_string += ' %s.' % rel['to']
+        hdf.setValue('%s.relation' % name, wiki_to_html(rel_string, hdf, env, db))
+        hdf.setValue('%s.from_id' % name, str(rel['from']))
+        hdf.setValue('%s.to_id' % name, str(rel['to']))
+        hdf.setValue('%s.type' % name, str(rel['type']))
+        hdf.setValue('%s.description' % name, env.get_config('relation:' + rel['type'], 'description', 'No description available'))
+        i += 1
+
+def _valid_ticket(ticket, db):
+    cursor = db.cursor()
+    cursor.execute('SELECT COUNT(*) FROM ticket WHERE id=%s', ticket)
+    row = cursor.fetchone()
+    if row:
+        if row['COUNT(*)'] > 0:
+            return True
+    return False
+
+def _valid_relation(relation, env):
+    relations = _get_valid_relations(env)
+    for i in relations:
+        if relation == i:
+            return True
+    return False
+
+def _get_valid_relations(env):
+    """Returns a list of all valid relations (fetched from configuration file).
+    To set valid relations, edit the configuration and change this part:
+    
+        [ticket]
+        valid_relations = depends on, related to, duplicate of
+
+    and add/remove the relations you want, i.e.
+
+        [ticket]
+        valid_relations = depends on, blocks, duplicate of, is subissue of
+
+    Each relation may also have a set of hooks associated with it. The valid hooks
+    to put functions in are:
+        hooks_ticket_modify
+        hooks_relation_add
+        hooks_relation_remove
+
+    The hooks are defined in the configuration file as follows:
+        [relation:<relation name>]
+        hooks_ticket_modify   = <function_name> [ , <function_name> ... ]
+        hooks_relation_add    = <function_name> [ , <function_name> ... ]
+        hooks_relation_remove = <function_name> [ , <function_name> ... ]
+
+    An example:
+        [relation:depends on]
+        hooks_relation_add    = check_circular
+    """
+
+    rels = env.get_config('ticket', 'valid_relations')
+    return [i.strip() for i in rels.split(',')]
+
 
 class NewticketModule(Module):
     template_name = 'newticket.cs'
@@ -369,6 +452,13 @@
 
         now = int(time.time())
 
+        cursor = self.db.cursor()
+        cursor.execute("SELECT source, target, context FROM xref WHERE source='ticket:%s' OR target='ticket:%s'", id, id)
+        rows = cursor.fetchall()
+        if rows:
+            for r in rows:
+                self.call_relation_hooks('hooks_ticket_modify', action, r[0], r[1], r[2], id)
+
         ticket.save_changes(self.db,
                             self.args.get('author', self.req.authname),
                             self.args.get('comment'),
@@ -378,6 +468,68 @@
         tn.notify(ticket, newticket=0, modtime=now)
         self.req.redirect(self.env.href.ticket(id))
 
+    def save_relation(self, id):
+        self.perm.assert_permission(perm.TICKET_MODIFY)
+        ticket = Ticket(self.db, id)
+        newrel_from = self.args.get('rel_from')
+        newrel_type = self.args.get('rel_type')
+        newrel_to = self.args.get('rel_to')
+
+        # Check sanity of arguments
+        if not newrel_from or not newrel_type or not newrel_to:
+            raise util.TracError('Ticket relations must have to, from and type fields.')
+        if not _valid_ticket(newrel_from, self.db) or not _valid_ticket(newrel_to, self.db):
+            raise util.TracError('Ticket relations must use existing tickets.')
+        if not _valid_relation(newrel_type, self.env):
+            raise util.TracError('Ticket relations must be in the set of valid relations: %s' % str(_get_valid_relations(self.env)))
+        if (not newrel_from == str(id) and not newrel_to == str(id)) or (newrel_from == newrel_to):
+            raise util.TracError('Either the from field or the to field, but not both, must be this ticket.')
+
+        # Call hooks
+        self.call_relation_hooks('hooks_relation_add', self.args.get('action', None), newrel_from, newrel_to, newrel_type, id)
+
+        # Insert into database and commit
+        cursor = self.db.cursor()
+        try:
+            cursor.execute("INSERT INTO xref VALUES (%s, %s, %s)", 'ticket:' + str(newrel_from), 'ticket:' + str(newrel_to), newrel_type)
+        except sqlite.IntegrityError:
+            self.db.rollback()
+            raise util.TracError('This relation already exists.')
+        cursor.close()
+        self.db.commit()
+
+    def remove_relation(self, id):
+        self.perm.assert_permission(perm.TICKET_MODIFY)
+        ticket = Ticket(self.db, id)
+        rel_from = self.args.get('rel_from')
+        rel_type = self.args.get('rel_type')
+        rel_to = self.args.get('rel_to')
+
+        # Check sanity.
+        if not rel_from or not rel_type or not rel_to:
+            raise util.TracError('Cannot remove relation: Required information is missing.')
+
+        # Call hooks
+        self.call_relation_hooks('hooks_relation_remove', self.args.get('action', None), rel_from, rel_to, rel_type, id)
+
+        # Commit to database.
+        cursor = self.db.cursor()
+        cursor.execute("DELETE FROM xref WHERE source=%s AND target=%s and context=%s", rel_from, rel_to, rel_type)
+        cursor.close()
+        self.db.commit()
+
+    def call_relation_hooks(self, hook_type, action, rel_from, rel_to, rel_type, id):
+        """Calls all hooks of the given hook_type."""
+        for hook_name in self.env.get_config('relation:' + rel_type,
+                hook_type).split(','):
+            hook_name = hook_name.strip()
+            if hook_name == '':
+                continue
+            if not hasattr(relation_hooks, hook_name):
+                raise util.TracError('Could not add relation: Hook "%s" not found.' % hook_name)
+            getattr(relation_hooks, hook_name)(self.db, self.env, rel_from, rel_to,
+                    rel_type, action, id)
+
     def insert_ticket_data(self, hdf, id, ticket, reporter_id):
         """Insert ticket data into the hdf"""
         evals = util.mydict(zip(ticket.keys(),
@@ -400,6 +552,8 @@
         util.hdf_add_if_missing(self.req.hdf, 'enums.severity', ticket['severity'])
         util.hdf_add_if_missing(self.req.hdf, 'enums.resolution', 'fixed')
 
+
+
         self.req.hdf.setValue('ticket.reporter_id', util.escape(reporter_id))
         self.req.hdf.setValue('title', '#%d (%s)' % (id, util.escape(ticket['summary'])))
         self.req.hdf.setValue('ticket.description.formatted',
@@ -427,6 +581,28 @@
             idx = idx + 1
 
         insert_custom_fields(self.env, hdf, ticket)
+        # Insert all relations in the database
+        insert_relation_fields(self.env, self.db, hdf, ticket.relations, id)
+
+        # Add list of all available tickets and valid relations
+        cursor = self.db.cursor()
+        cursor.execute('SELECT SUBSTR(id, 0, 1024) FROM ticket ORDER BY id')
+        rows = cursor.fetchall()
+        ctr = 0
+        if rows:
+            for r in rows:
+                if str(r[0]) == str(id):
+                    hdf.setValue('ticket.all_tickets.%d.name' % ctr, 'This ticket')
+                else:
+                    hdf.setValue('ticket.all_tickets.%d.name' % ctr, r[0])
+                hdf.setValue('ticket.all_tickets.%d.value' % ctr, r[0])
+                ctr += 1
+
+        ctr = 0
+        for i in _get_valid_relations(self.env):
+            hdf.setValue('ticket.valid_relations.%d.name' % ctr, i)
+            ctr += 1
+
         # List attached files
         self.env.get_attachments_hdf(self.db, 'ticket', str(id), self.req.hdf,
                                      'ticket.attachments')
@@ -440,12 +616,18 @@
         if not self.args.has_key('id'):
             self.req.redirect(self.env.href.wiki())
 
+
         id = int(self.args.get('id'))
 
         if not preview \
                and action in ['leave', 'accept', 'reopen', 'resolve', 'reassign']:
             self.save_changes (id)
 
+        if not preview and self.args.has_key('newrel_submit'):
+            self.save_relation(id)
+        if not preview and self.args.has_key('removerel_submit'):
+            self.remove_relation(id)
+
         ticket = Ticket(self.db, id)
         reporter_id = util.get_reporter_id(self.req)
 
diff -Naur trac-0.8.1-orig/trac/db_default.py trac-0.8.1/trac/db_default.py
--- trac-0.8.1-orig/trac/db_default.py	2004-11-18 13:46:28.000000000 +0100
+++ trac-0.8.1/trac/db_default.py	2005-03-14 16:26:35.386012440 +0100
@@ -111,6 +111,13 @@
        value            text,
        UNIQUE(ticket,name)
 );
+
+CREATE TABLE xref (
+       source           text,
+       target           text,
+       context          text
+);
+
 CREATE TABLE report (
         id              integer PRIMARY KEY,
         author          text,
diff -Naur trac-0.8.1-orig/trac/relation_hooks.py trac-0.8.1/trac/relation_hooks.py
--- trac-0.8.1-orig/trac/relation_hooks.py	1970-01-01 01:00:00.000000000 +0100
+++ trac-0.8.1/trac/relation_hooks.py	2005-03-14 16:26:40.826220785 +0100
@@ -0,0 +1,89 @@
+import util
+
+def dummy(db, env, ticket_from, ticket_to, relation_type, action, target_ticket):
+    """A dummy hook that does nothing."""
+    return
+
+def check_circular(db, env, ticket_from, ticket_to, relation_type, action, target_ticket):
+    """Checks for a circular relation between the two tickets."""
+    _circular(db, ticket_from, ticket_to, relation_type)
+    return
+
+def _circular(db, target_ticket, origin, relation_type):
+    cursor = db.cursor()
+    cursor.execute('SELECT target FROM xref WHERE source=%s AND context=%s', origin, relation_type)
+    rows = cursor.fetchall()
+    if rows:
+        for r in rows:
+            if r[0] == target_ticket:
+                cursor.close()
+                raise util.TracError('This relation creates a circular relationship between tickets, and cannot be created.')
+            _circular(db, target_ticket, r[0], relation_type)
+    cursor.close()
+
+
+def check_excludes(db, env, ticket_from, ticket_to, relation_type, action, target_ticket):
+    """Checks if there already is a relation between the two tickets that excludes
+    the relation we are trying to add."""
+    excludes = env.get_config('relation:' + relation_type, 'excludes_relations')
+    if not excludes:
+        return
+    excludes = excludes.split(',')
+    cursor = db.cursor()
+    sql = 'SELECT COUNT(*) FROM xref WHERE source=%s AND target=%s AND'
+    where = []
+    args = [ticket_from, ticket_to]
+    for i in excludes:
+        where.append('context=%s')
+        args.append(i)
+    sql += ('(%s)') % " OR ".join(where)
+    cursor.execute(sql, *args)
+    row = cursor.fetchone()
+    if row:
+        if row[0] > 0:
+            raise util.TracError('Cannot add relation; another relation which excludes this one already exists.')
+
+def check_from_tickets_closed(db, env, ticket_from, ticket_to, relation_type, action, target_ticket):
+    """This hook checks that closed(source) => closed(target) and
+    open(source) => open(target) for all targets which have a relation
+    of type relation_type from source to target."""
+    if not (action == 'resolve' or action == 'reopen'):
+        return
+    cursor = db.cursor()
+    if action == 'resolve' and ticket_to == target_ticket:
+        cursor.execute("SELECT COUNT(*) FROM xref r, ticket t WHERE r.context=%s AND r.source='ticket:'+t.id AND t.status != 'closed' AND r.target=%s", relation_type, target_ticket)
+    elif action == 'reopen' and ticket_from == target_ticket:
+        cursor.execute("SELECT COUNT(*) FROM xref r, ticket t WHERE r.context=%s AND r.source=%s AND t.status=\'closed\' AND r.target='ticket:'+t.id", relation_type, target_ticket)
+    else:
+        return
+    row = cursor.fetchone()
+    if row and row[0] > 0:
+        if action == 'resolve':
+            raise util.TracError('One or more of this ticket\'s relations require another ticket to be resolved before this one can be resolved.')
+        elif action == 'reopen':
+            raise util.TracError('One or more of this ticket\'s relations require another ticket to be reopened before this one can be reopened.')
+
+def check_to_tickets_closed(db, env, ticket_from, ticket_to, relation_type, action, target_ticket):
+    """This hook checks that closed(source) => closed(target) and
+    open(source) => open(target) for all targets which have a relation
+    of type relation_type from source to target."""
+    if not (action == 'resolve' or action == 'reopen'):
+        return
+    cursor = db.cursor()
+    if action == 'resolve' and ticket_from == target_ticket:
+        cursor.execute("SELECT COUNT(*) FROM xref r, ticket t WHERE r.context=%s AND r.target='ticket:'+t.id AND t.status != 'closed' AND r.source=%s", relation_type, target_ticket)
+    elif action == 'reopen' and ticket_to == target_ticket:
+        cursor.execute("SELECT COUNT(*) FROM xref r, ticket t WHERE r.context=%s AND r.target=%s AND t.status='closed' AND r.source='ticket:'+t.id", relation_type, target_ticket)
+    else:
+        return
+    row = cursor.fetchone()
+    if row and row[0] > 0:
+        if action == 'resolve':
+            raise util.TracError('One or more of this ticket\'s relations require another ticket to be resolved before this one can be resolved.')
+        elif action == 'reopen':
+            raise util.TracError('One or more of this ticket\'s relations require another ticket to be reopened before this one can be reopened.')
+    return
+
+def failing(db, env, ticket_from, ticket_to, relation_type, action, target_ticket):
+    """A hook that always fails, for testing purposes."""
+    raise util.TracError("The failing hook failed.")
