diff mbox series

[v3,1/1] management: introduce replacerelations command

Message ID 20200702150631.2756-2-rohitsarkar5398@gmail.com
State Superseded
Headers show
Series add replacerelations management command | expand

Commit Message

Rohit Sarkar July 2, 2020, 3:06 p.m. UTC
The replacerelations script is used to ingest relations into Patchwork's
patch database. A patch groups file is taken as input, which on each
line contains a space separated list of patchwork ids denoting a
relation. All the existing relations in Patchwork's database are removed
and the relations read from the patch groups file are ingested.

Signed-off-by: Rohit Sarkar <rohitsarkar5398@gmail.com>
---
 docs/deployment/management.rst                | 26 +++++++
 .../management/commands/replacerelations.py   | 72 +++++++++++++++++++
 patchwork/tests/__init__.py                   |  1 +
 patchwork/tests/relations/patch-groups        |  3 +
 .../relations/patch-groups-missing-patch-ids  |  4 ++
 patchwork/tests/test_management.py            | 27 ++++++-
 6 files changed, 132 insertions(+), 1 deletion(-)
 create mode 100644 patchwork/management/commands/replacerelations.py
 create mode 100644 patchwork/tests/relations/patch-groups
 create mode 100644 patchwork/tests/relations/patch-groups-missing-patch-ids

Comments

Daniel Axtens July 20, 2020, 5:34 a.m. UTC | #1
Hi Rohit,

Apologies for the delay.

My one comment is that the code silently ignores patch IDs that are not
in the database. I don't really mind that much (although I probably
would have thrown a warning?), and I'm pleased that the tests cover it,
but it's completely undocumented. Please could you add a note about that
to the documentation.

> +This is a script used to ingest relations into Patchwork.
> +A patch groups file contains on each line patchwork ids of patches that form a relation.

Within the context of patchwork we call them 'patch IDs', not 'patchwork ids'. 

> +Eg contents of a patch groups file:

'Eg' should either have dots ('E.g.') or preferably be spelled out ("For
example, ...")

> +
> +    1 3 5
> +    2
> +    7 10 11 12
> +
> +In this case the script will identify 2 relations, (1, 3, 5) and (7, 10, 11, 12).

It would also be good to highlight that a line with only 1 patch will be
ignored as it's not possible to create a relation with only one patch.

> +Running this script will remove all existing relations and replace them with the relations found in the file.

Please could you also wrap your lines as in the rest of the file.

> +
> +.. option:: infile
> +
> +    input patch groups file.
> +


Lastly, when applying the patch, I see:

dja@dja-thinkpad ~/d/p/patchwork (master)> git pw patch apply 1321548 -s
.git/rebase-apply/patch:80: trailing whitespace.
    
.git/rebase-apply/patch:81: trailing whitespace.
    def handle(self, *args, **options):   
.git/rebase-apply/patch:98: trailing whitespace.
        
.git/rebase-apply/patch:102: trailing whitespace.
        
.git/rebase-apply/patch:122: trailing whitespace.
                
warning: squelched 4 whitespace errors
warning: 9 lines add whitespace errors.
Applying: management: introduce replacerelations command

Could you please fix those whitespace errors?

Kind regards,
Daniel

>  rehash
>  ~~~~~~
>  
> diff --git a/patchwork/management/commands/replacerelations.py b/patchwork/management/commands/replacerelations.py
> new file mode 100644
> index 0000000..0d9581d
> --- /dev/null
> +++ b/patchwork/management/commands/replacerelations.py
> @@ -0,0 +1,72 @@
> +# Patchwork - automated patch tracking system
> +# Copyright (C) 2020 Rohit Sarkar <rohitsarkar5398@gmail.com>
> +#
> +# SPDX-License-Identifier: GPL-2.0-or-later
> +
> +import logging
> +import os
> +import sys
> +
> +from django.db import transaction
> +from django.core.management.base import BaseCommand
> +
> +from patchwork.models import Patch
> +from patchwork.models import PatchRelation
> +
> +logger = logging.getLogger(__name__)
> +
> +class Command(BaseCommand):
> +    help = 'Parse a relations file generated by PaStA and replace existing relations with the ones parsed'
> +
> +    def add_arguments(self, parser):
> +        parser.add_argument(
> +            'infile',
> +            help='input relations filename')
> +    
> +    def handle(self, *args, **options):   
> +        verbosity = int(options['verbosity'])
> +        if not verbosity:
> +            level = logging.CRITICAL
> +        elif verbosity == 1:
> +            level = logging.ERROR
> +        elif verbosity == 2:
> +            level = logging.INFO
> +        else:
> +            level = logging.DEBUG
> +
> +        logger.setLevel(level)
> +
> +        path = args and args[0] or options['infile']
> +        if not os.path.exists(path):
> +            logger.error('Invalid path: %s', path)
> +            sys.exit(1)
> +        
> +
> +        with open(path, 'r') as f:
> +            lines = f.readlines()
> +        
> +        # filter out trailing empty lines
> +        while len(lines) and not lines[-1]:
> +            lines.pop()
> +
> +        relations = [line.split(' ') for line in lines]
> +
> +        with transaction.atomic():
> +            PatchRelation.objects.all().delete()
> +            count = len(relations)
> +            ingested = 0
> +            logger.info('Parsing %d relations' % count)
> +            for i, patch_ids in enumerate(relations):
> +                related_patches = Patch.objects.filter(id__in=patch_ids)
> +
> +                if len(related_patches) > 1:
> +                    relation = PatchRelation()
> +                    relation.save()
> +                    related_patches.update(related=relation)
> +                    ingested += 1
> +                
> +                if i % 10 == 0:
> +                    self.stdout.write('%06d/%06d\r' % (i, count), ending='')
> +                    self.stdout.flush()
> +            
> +            self.stdout.write('Ingested %d relations' % ingested)
> diff --git a/patchwork/tests/__init__.py b/patchwork/tests/__init__.py
> index 8f78ea7..83358a6 100644
> --- a/patchwork/tests/__init__.py
> +++ b/patchwork/tests/__init__.py
> @@ -8,3 +8,4 @@ import os
>  TEST_MAIL_DIR = os.path.join(os.path.dirname(__file__), 'mail')
>  TEST_PATCH_DIR = os.path.join(os.path.dirname(__file__), 'patches')
>  TEST_FUZZ_DIR = os.path.join(os.path.dirname(__file__), 'fuzztests')
> +TEST_RELATIONS_DIR = os.path.join(os.path.dirname(__file__), 'relations')
> diff --git a/patchwork/tests/relations/patch-groups b/patchwork/tests/relations/patch-groups
> new file mode 100644
> index 0000000..593cf0f
> --- /dev/null
> +++ b/patchwork/tests/relations/patch-groups
> @@ -0,0 +1,3 @@
> +1 2
> +3 4 5
> +6
> diff --git a/patchwork/tests/relations/patch-groups-missing-patch-ids b/patchwork/tests/relations/patch-groups-missing-patch-ids
> new file mode 100644
> index 0000000..091b195
> --- /dev/null
> +++ b/patchwork/tests/relations/patch-groups-missing-patch-ids
> @@ -0,0 +1,4 @@
> +1 2
> +3 4 5 7
> +6 8
> +9 10
> diff --git a/patchwork/tests/test_management.py b/patchwork/tests/test_management.py
> index 66c6bad..c648cc7 100644
> --- a/patchwork/tests/test_management.py
> +++ b/patchwork/tests/test_management.py
> @@ -11,7 +11,7 @@ from django.core.management import call_command
>  from django.test import TestCase
>  
>  from patchwork import models
> -from patchwork.tests import TEST_MAIL_DIR
> +from patchwork.tests import TEST_MAIL_DIR, TEST_RELATIONS_DIR
>  from patchwork.tests import utils
>  
>  
> @@ -124,3 +124,28 @@ class ParsearchiveTest(TestCase):
>  
>          self.assertIn('Processed 1 messages -->', out.getvalue())
>          self.assertIn('  1 dropped', out.getvalue())
> +
> +class ReplacerelationsTest(TestCase):
> +    def test_invalid_path(self):
> +        out = StringIO()
> +        with self.assertRaises(SystemExit) as exc:
> +            call_command('replacerelations', 'xyz123random', stdout=out)
> +        self.assertEqual(exc.exception.code, 1)
> +
> +    def test_valid_relations(self):
> +        utils.create_patches(6)
> +        out = StringIO()
> +        call_command('replacerelations',
> +                     os.path.join(TEST_RELATIONS_DIR,
> +                                  'patch-groups'),
> +                                  stdout=out)
> +        
> +        self.assertEqual(models.PatchRelation.objects.count(), 2)
> +
> +        call_command('replacerelations',
> +                     os.path.join(TEST_RELATIONS_DIR,
> +                                  'patch-groups-missing-patch-ids'),
> +                                  stdout=out)
> +        
> +        self.assertEqual(models.PatchRelation.objects.count(), 2)
> +
> -- 
> 2.23.0.385.gbc12974a89
diff mbox series

Patch

diff --git a/docs/deployment/management.rst b/docs/deployment/management.rst
index 9c57f19..375fb5a 100644
--- a/docs/deployment/management.rst
+++ b/docs/deployment/management.rst
@@ -116,6 +116,32 @@  the :ref:`deployment installation guide <deployment-parsemail>`.
 
    input mbox filename. If not supplied, a patch will be read from ``stdin``.
 
+replacerelations
+~~~~~~~~~~~~~~~~
+
+.. program:: manage.py replacerelations
+
+Parse a patch groups file and store any relation found
+
+.. code-block:: shell
+
+    ./manage.py replacerelations <infile>
+
+This is a script used to ingest relations into Patchwork.
+A patch groups file contains on each line patchwork ids of patches that form a relation.
+Eg contents of a patch groups file:
+
+    1 3 5
+    2
+    7 10 11 12
+
+In this case the script will identify 2 relations, (1, 3, 5) and (7, 10, 11, 12).
+Running this script will remove all existing relations and replace them with the relations found in the file.
+
+.. option:: infile
+
+    input patch groups file.
+
 rehash
 ~~~~~~
 
diff --git a/patchwork/management/commands/replacerelations.py b/patchwork/management/commands/replacerelations.py
new file mode 100644
index 0000000..0d9581d
--- /dev/null
+++ b/patchwork/management/commands/replacerelations.py
@@ -0,0 +1,72 @@ 
+# Patchwork - automated patch tracking system
+# Copyright (C) 2020 Rohit Sarkar <rohitsarkar5398@gmail.com>
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import logging
+import os
+import sys
+
+from django.db import transaction
+from django.core.management.base import BaseCommand
+
+from patchwork.models import Patch
+from patchwork.models import PatchRelation
+
+logger = logging.getLogger(__name__)
+
+class Command(BaseCommand):
+    help = 'Parse a relations file generated by PaStA and replace existing relations with the ones parsed'
+
+    def add_arguments(self, parser):
+        parser.add_argument(
+            'infile',
+            help='input relations filename')
+    
+    def handle(self, *args, **options):   
+        verbosity = int(options['verbosity'])
+        if not verbosity:
+            level = logging.CRITICAL
+        elif verbosity == 1:
+            level = logging.ERROR
+        elif verbosity == 2:
+            level = logging.INFO
+        else:
+            level = logging.DEBUG
+
+        logger.setLevel(level)
+
+        path = args and args[0] or options['infile']
+        if not os.path.exists(path):
+            logger.error('Invalid path: %s', path)
+            sys.exit(1)
+        
+
+        with open(path, 'r') as f:
+            lines = f.readlines()
+        
+        # filter out trailing empty lines
+        while len(lines) and not lines[-1]:
+            lines.pop()
+
+        relations = [line.split(' ') for line in lines]
+
+        with transaction.atomic():
+            PatchRelation.objects.all().delete()
+            count = len(relations)
+            ingested = 0
+            logger.info('Parsing %d relations' % count)
+            for i, patch_ids in enumerate(relations):
+                related_patches = Patch.objects.filter(id__in=patch_ids)
+
+                if len(related_patches) > 1:
+                    relation = PatchRelation()
+                    relation.save()
+                    related_patches.update(related=relation)
+                    ingested += 1
+                
+                if i % 10 == 0:
+                    self.stdout.write('%06d/%06d\r' % (i, count), ending='')
+                    self.stdout.flush()
+            
+            self.stdout.write('Ingested %d relations' % ingested)
diff --git a/patchwork/tests/__init__.py b/patchwork/tests/__init__.py
index 8f78ea7..83358a6 100644
--- a/patchwork/tests/__init__.py
+++ b/patchwork/tests/__init__.py
@@ -8,3 +8,4 @@  import os
 TEST_MAIL_DIR = os.path.join(os.path.dirname(__file__), 'mail')
 TEST_PATCH_DIR = os.path.join(os.path.dirname(__file__), 'patches')
 TEST_FUZZ_DIR = os.path.join(os.path.dirname(__file__), 'fuzztests')
+TEST_RELATIONS_DIR = os.path.join(os.path.dirname(__file__), 'relations')
diff --git a/patchwork/tests/relations/patch-groups b/patchwork/tests/relations/patch-groups
new file mode 100644
index 0000000..593cf0f
--- /dev/null
+++ b/patchwork/tests/relations/patch-groups
@@ -0,0 +1,3 @@ 
+1 2
+3 4 5
+6
diff --git a/patchwork/tests/relations/patch-groups-missing-patch-ids b/patchwork/tests/relations/patch-groups-missing-patch-ids
new file mode 100644
index 0000000..091b195
--- /dev/null
+++ b/patchwork/tests/relations/patch-groups-missing-patch-ids
@@ -0,0 +1,4 @@ 
+1 2
+3 4 5 7
+6 8
+9 10
diff --git a/patchwork/tests/test_management.py b/patchwork/tests/test_management.py
index 66c6bad..c648cc7 100644
--- a/patchwork/tests/test_management.py
+++ b/patchwork/tests/test_management.py
@@ -11,7 +11,7 @@  from django.core.management import call_command
 from django.test import TestCase
 
 from patchwork import models
-from patchwork.tests import TEST_MAIL_DIR
+from patchwork.tests import TEST_MAIL_DIR, TEST_RELATIONS_DIR
 from patchwork.tests import utils
 
 
@@ -124,3 +124,28 @@  class ParsearchiveTest(TestCase):
 
         self.assertIn('Processed 1 messages -->', out.getvalue())
         self.assertIn('  1 dropped', out.getvalue())
+
+class ReplacerelationsTest(TestCase):
+    def test_invalid_path(self):
+        out = StringIO()
+        with self.assertRaises(SystemExit) as exc:
+            call_command('replacerelations', 'xyz123random', stdout=out)
+        self.assertEqual(exc.exception.code, 1)
+
+    def test_valid_relations(self):
+        utils.create_patches(6)
+        out = StringIO()
+        call_command('replacerelations',
+                     os.path.join(TEST_RELATIONS_DIR,
+                                  'patch-groups'),
+                                  stdout=out)
+        
+        self.assertEqual(models.PatchRelation.objects.count(), 2)
+
+        call_command('replacerelations',
+                     os.path.join(TEST_RELATIONS_DIR,
+                                  'patch-groups-missing-patch-ids'),
+                                  stdout=out)
+        
+        self.assertEqual(models.PatchRelation.objects.count(), 2)
+