summaryrefslogtreecommitdiff
path: root/tools/patman/database.py
diff options
context:
space:
mode:
authorTom Rini <trini@konsulko.com>2026-07-06 08:12:19 -0600
committerTom Rini <trini@konsulko.com>2026-07-06 08:28:51 -0600
commit18761722e67d6e3770ab296a04fe3d40022e76d2 (patch)
treee544dae54d83d5d3524e527c435be0395efe3a17 /tools/patman/database.py
parenta25c9bd19215ddd95d7494ebf8d842c46fdf3ba8 (diff)
parent91b94aff149d2c6efe3afa81adaf5edcc6119221 (diff)
downloadu-boot-18761722e67d6e3770ab296a04fe3d40022e76d2.tar.gz
u-boot-18761722e67d6e3770ab296a04fe3d40022e76d2.zip
Merge patch series "Remove patman from the U-Boot tree"
Simon Glass <sjg@chromium.org> says: patman is now maintained as a standalone 'patch-manager' package, so remove it from the tree. The command becomes a stub that tells people to run 'pip install patch-manager'. buildman still imports the shared modules commit and patchstream (along with their dependencies), so this series leaves those in place. It drops the tool's code, tests, CI hooks and packaging, and removes the in-tree documentation, moving the b4 contributor guide alongside the patman note in the patch-sending docs. It also adds a .patman-defaults file so the external tool is set up for U-Boot, next to the existing .b4-config. Where the CI jobs relied on patman's requirements for the setuptools that pylibfdt needs, they now install scripts/dtc/pylibfdt/requirements.txt instead. More could be done here: commit and patchstream (and their dependencies series, get_maintainer and settings) only remain because buildman still imports them. A follow-up could move those into u_boot_pylib (or buildman itself) and drop the rest, leaving tools/patman as just the stub. Link: https://lore.kernel.org/r/20260705193251.2190705-1-sjg@chromium.org
Diffstat (limited to 'tools/patman/database.py')
-rw-r--r--tools/patman/database.py823
1 files changed, 0 insertions, 823 deletions
diff --git a/tools/patman/database.py b/tools/patman/database.py
deleted file mode 100644
index 9c25b04a720..00000000000
--- a/tools/patman/database.py
+++ /dev/null
@@ -1,823 +0,0 @@
-# SPDX-License-Identifier: GPL-2.0+
-#
-# Copyright 2025 Simon Glass <sjg@chromium.org>
-#
-"""Handles the patman database
-
-This uses sqlite3 with a local file.
-
-To adjsut the schema, increment LATEST, create a migrate_to_v<x>() function
-and write some code in migrate_to() to call it.
-"""
-
-from collections import namedtuple, OrderedDict
-import os
-import sqlite3
-
-from u_boot_pylib import tools
-from u_boot_pylib import tout
-from patman.series import Series
-
-# Schema version (version 0 means there is no database yet)
-LATEST = 4
-
-# Information about a series/version record
-SerVer = namedtuple(
- 'SER_VER',
- 'idnum,series_id,version,link,cover_id,cover_num_comments,name,'
- 'archive_tag')
-
-# Record from the pcommit table:
-# idnum (int): record ID
-# seq (int): Patch sequence in series (0 is first)
-# subject (str): patch subject
-# svid (int): ID of series/version record in ser_ver table
-# change_id (str): Change-ID value
-# state (str): Current status in patchwork
-# patch_id (int): Patchwork's patch ID for this patch
-# num_comments (int): Number of comments attached to the commit
-Pcommit = namedtuple(
- 'PCOMMIT',
- 'idnum,seq,subject,svid,change_id,state,patch_id,num_comments')
-
-
-class Database:
- """Database of information used by patman"""
-
- # dict of databases:
- # key: filename
- # value: Database object
- instances = {}
-
- def __init__(self, db_path):
- """Set up a new database object
-
- Args:
- db_path (str): Path to the database
- """
- if db_path in Database.instances:
- # Two connections to the database can cause:
- # sqlite3.OperationalError: database is locked
- raise ValueError(f"There is already a database for '{db_path}'")
- self.con = None
- self.cur = None
- self.db_path = db_path
- self.is_open = False
- Database.instances[db_path] = self
-
- @staticmethod
- def get_instance(db_path):
- """Get the database instance for a path
-
- This is provides to ensure that different callers can obtain the
- same database object when accessing the same database file.
-
- Args:
- db_path (str): Path to the database
-
- Return:
- Database: Database instance, which is created if necessary
- """
- db = Database.instances.get(db_path)
- if db:
- return db, False
- return Database(db_path), True
-
- def start(self):
- """Open the database read for use, migrate to latest schema"""
- self.open_it()
- self.migrate_to(LATEST)
-
- def open_it(self):
- """Open the database, creating it if necessary"""
- if self.is_open:
- raise ValueError('Already open')
- if not os.path.exists(self.db_path):
- tout.warning(f'Creating new database {self.db_path}')
- self.con = sqlite3.connect(self.db_path)
- self.cur = self.con.cursor()
- self.is_open = True
-
- def close(self):
- """Close the database"""
- if not self.is_open:
- raise ValueError('Already closed')
- self.con.close()
- self.cur = None
- self.con = None
- self.is_open = False
-
- def create_v1(self):
- """Create a database with the v1 schema"""
- self.cur.execute(
- 'CREATE TABLE series (id INTEGER PRIMARY KEY AUTOINCREMENT,'
- 'name UNIQUE, desc, archived BIT)')
-
- # Provides a series_id/version pair, which is used to refer to a
- # particular series version sent to patchwork. This stores the link
- # to patchwork
- self.cur.execute(
- 'CREATE TABLE ser_ver (id INTEGER PRIMARY KEY AUTOINCREMENT,'
- 'series_id INTEGER, version INTEGER, link,'
- 'FOREIGN KEY (series_id) REFERENCES series (id))')
-
- self.cur.execute(
- 'CREATE TABLE upstream (name UNIQUE, url, is_default BIT)')
-
- # change_id is the Change-Id
- # patch_id is the ID of the patch on the patchwork server
- self.cur.execute(
- 'CREATE TABLE pcommit (id INTEGER PRIMARY KEY AUTOINCREMENT,'
- 'svid INTEGER, seq INTEGER, subject, patch_id INTEGER, '
- 'change_id, state, num_comments INTEGER, '
- 'FOREIGN KEY (svid) REFERENCES ser_ver (id))')
-
- self.cur.execute(
- 'CREATE TABLE settings (name UNIQUE, proj_id INT, link_name)')
-
- def _migrate_to_v2(self):
- """Add a schema_version table"""
- self.cur.execute('CREATE TABLE schema_version (version INTEGER)')
-
- def _migrate_to_v3(self):
- """Store the number of cover-letter comments in the schema"""
- self.cur.execute('ALTER TABLE ser_ver ADD COLUMN cover_id')
- self.cur.execute('ALTER TABLE ser_ver ADD COLUMN cover_num_comments '
- 'INTEGER')
- self.cur.execute('ALTER TABLE ser_ver ADD COLUMN name')
-
- def _migrate_to_v4(self):
- """Add an archive tag for each ser_ver"""
- self.cur.execute('ALTER TABLE ser_ver ADD COLUMN archive_tag')
-
- def migrate_to(self, dest_version):
- """Migrate the database to the selected version
-
- Args:
- dest_version (int): Version to migrate to
- """
- while True:
- version = self.get_schema_version()
- if version == dest_version:
- break
-
- self.close()
- tools.write_file(f'{self.db_path}old.v{version}',
- tools.read_file(self.db_path))
-
- version += 1
- tout.info(f'Update database to v{version}')
- self.open_it()
- if version == 1:
- self.create_v1()
- elif version == 2:
- self._migrate_to_v2()
- elif version == 3:
- self._migrate_to_v3()
- elif version == 4:
- self._migrate_to_v4()
-
- # Save the new version if we have a schema_version table
- if version > 1:
- self.cur.execute('DELETE FROM schema_version')
- self.cur.execute(
- 'INSERT INTO schema_version (version) VALUES (?)',
- (version,))
- self.commit()
-
- def get_schema_version(self):
- """Get the version of the database's schema
-
- Return:
- int: Database version, 0 means there is no data; anything less than
- LATEST means the schema is out of date and must be updated
- """
- # If there is no database at all, assume v0
- version = 0
- try:
- self.cur.execute('SELECT name FROM series')
- except sqlite3.OperationalError:
- return 0
-
- # If there is no schema, assume v1
- try:
- self.cur.execute('SELECT version FROM schema_version')
- version = self.cur.fetchone()[0]
- except sqlite3.OperationalError:
- return 1
- return version
-
- def execute(self, query, parameters=()):
- """Execute a database query
-
- Args:
- query (str): Query string
- parameters (list of values): Parameters to pass
-
- Return:
-
- """
- return self.cur.execute(query, parameters)
-
- def commit(self):
- """Commit changes to the database"""
- self.con.commit()
-
- def rollback(self):
- """Roll back changes to the database"""
- self.con.rollback()
-
- def lastrowid(self):
- """Get the last row-ID reported by the database
-
- Return:
- int: Value for lastrowid
- """
- return self.cur.lastrowid
-
- def rowcount(self):
- """Get the row-count reported by the database
-
- Return:
- int: Value for rowcount
- """
- return self.cur.rowcount
-
- def _get_series_list(self, include_archived):
- """Get a list of Series objects from the database
-
- Args:
- include_archived (bool): True to include archives series
-
- Return:
- list of Series
- """
- res = self.execute(
- 'SELECT id, name, desc FROM series ' +
- ('WHERE archived = 0' if not include_archived else ''))
- return [Series.from_fields(idnum=idnum, name=name, desc=desc)
- for idnum, name, desc in res.fetchall()]
-
- # series functions
-
- def series_get_dict_by_id(self, include_archived=False):
- """Get a dict of Series objects from the database
-
- Args:
- include_archived (bool): True to include archives series
-
- Return:
- OrderedDict:
- key: series ID
- value: Series with idnum, name and desc filled out
- """
- sdict = OrderedDict()
- for ser in self._get_series_list(include_archived):
- sdict[ser.idnum] = ser
- return sdict
-
- def series_find_by_name(self, name, include_archived=False):
- """Find a series and return its details
-
- Args:
- name (str): Name to search for
- include_archived (bool): True to include archives series
-
- Returns:
- idnum, or None if not found
- """
- res = self.execute(
- 'SELECT id FROM series WHERE name = ?' +
- ('AND archived = 0' if not include_archived else ''), (name,))
- recs = res.fetchall()
-
- # This shouldn't happen
- assert len(recs) <= 1, 'Expected one match, but multiple found'
-
- if len(recs) != 1:
- return None
- return recs[0][0]
-
- def series_get_info(self, idnum):
- """Get information for a series from the database
-
- Args:
- idnum (int): Series ID to look up
-
- Return: tuple:
- str: Series name
- str: Series description
-
- Raises:
- ValueError: Series is not found
- """
- res = self.execute('SELECT name, desc FROM series WHERE id = ?',
- (idnum,))
- recs = res.fetchall()
- if len(recs) != 1:
- raise ValueError(f'No series found (id {idnum} len {len(recs)})')
- return recs[0]
-
- def series_get_dict(self, include_archived=False):
- """Get a dict of Series objects from the database
-
- Args:
- include_archived (bool): True to include archives series
-
- Return:
- OrderedDict:
- key: series name
- value: Series with idnum, name and desc filled out
- """
- sdict = OrderedDict()
- for ser in self._get_series_list(include_archived):
- sdict[ser.name] = ser
- return sdict
-
- def series_get_version_list(self, series_idnum):
- """Get a list of the versions available for a series
-
- Args:
- series_idnum (int): ID of series to look up
-
- Return:
- str: List of versions, which may be empty if the series is in the
- process of being added
- """
- res = self.execute('SELECT version FROM ser_ver WHERE series_id = ?',
- (series_idnum,))
- return [x[0] for x in res.fetchall()]
-
- def series_get_max_version(self, series_idnum):
- """Get the highest version number available for a series
-
- Args:
- series_idnum (int): ID of series to look up
-
- Return:
- int: Maximum version number
- """
- res = self.execute(
- 'SELECT MAX(version) FROM ser_ver WHERE series_id = ?',
- (series_idnum,))
- return res.fetchall()[0][0]
-
- def series_get_all_max_versions(self):
- """Find the latest version of all series
-
- Return: list of:
- int: ser_ver ID
- int: series ID
- int: Maximum version
- """
- res = self.execute(
- 'SELECT id, series_id, MAX(version) FROM ser_ver '
- 'GROUP BY series_id')
- return res.fetchall()
-
- def series_add(self, name, desc):
- """Add a new series record
-
- The new record is set to not archived
-
- Args:
- name (str): Series name
- desc (str): Series description
-
- Return:
- int: ID num of the new series record
- """
- self.execute(
- 'INSERT INTO series (name, desc, archived) '
- f"VALUES ('{name}', '{desc}', 0)")
- return self.lastrowid()
-
- def series_remove(self, idnum):
- """Remove a series from the database
-
- The series must exist
-
- Args:
- idnum (int): ID num of series to remove
- """
- self.execute('DELETE FROM series WHERE id = ?', (idnum,))
- assert self.rowcount() == 1
-
- def series_remove_by_name(self, name):
- """Remove a series from the database
-
- Args:
- name (str): Name of series to remove
-
- Raises:
- ValueError: Series does not exist (database is rolled back)
- """
- self.execute('DELETE FROM series WHERE name = ?', (name,))
- if self.rowcount() != 1:
- self.rollback()
- raise ValueError(f"No such series '{name}'")
-
- def series_set_archived(self, series_idnum, archived):
- """Update archive flag for a series
-
- Args:
- series_idnum (int): ID num of the series
- archived (bool): Whether to mark the series as archived or
- unarchived
- """
- self.execute(
- 'UPDATE series SET archived = ? WHERE id = ?',
- (archived, series_idnum))
-
- def series_set_name(self, series_idnum, name):
- """Update name for a series
-
- Args:
- series_idnum (int): ID num of the series
- name (str): new name to use
- """
- self.execute(
- 'UPDATE series SET name = ? WHERE id = ?', (name, series_idnum))
-
- # ser_ver functions
-
- def ser_ver_get_link(self, series_idnum, version):
- """Get the link for a series version
-
- Args:
- series_idnum (int): ID num of the series
- version (int): Version number to search for
-
- Return:
- str: Patchwork link as a string, e.g. '12325', or None if none
-
- Raises:
- ValueError: Multiple matches are found
- """
- res = self.execute(
- 'SELECT link FROM ser_ver WHERE '
- f"series_id = {series_idnum} AND version = '{version}'")
- recs = res.fetchall()
- if not recs:
- return None
- if len(recs) > 1:
- raise ValueError('Expected one match, but multiple matches found')
- return recs[0][0]
-
- def ser_ver_set_link(self, series_idnum, version, link):
- """Set the link for a series version
-
- Args:
- series_idnum (int): ID num of the series
- version (int): Version number to search for
- link (str): Patchwork link for the ser_ver
-
- Return:
- bool: True if the record was found and updated, else False
- """
- if link is None:
- link = ''
- self.execute(
- 'UPDATE ser_ver SET link = ? WHERE series_id = ? AND version = ?',
- (str(link), series_idnum, version))
- return self.rowcount() != 0
-
- def ser_ver_set_info(self, info):
- """Set the info for a series version
-
- Args:
- info (SER_VER): Info to set. Only two options are supported:
- 1: svid,cover_id,cover_num_comments,name
- 2: svid,name
-
- Return:
- bool: True if the record was found and updated, else False
- """
- assert info.idnum is not None
- if info.cover_id:
- assert info.series_id is None
- self.execute(
- 'UPDATE ser_ver SET cover_id = ?, cover_num_comments = ?, '
- 'name = ? WHERE id = ?',
- (info.cover_id, info.cover_num_comments, info.name,
- info.idnum))
- else:
- assert not info.cover_id
- assert not info.cover_num_comments
- assert not info.series_id
- assert not info.version
- assert not info.link
- self.execute('UPDATE ser_ver SET name = ? WHERE id = ?',
- (info.name, info.idnum))
-
- return self.rowcount() != 0
-
- def ser_ver_set_version(self, svid, version):
- """Sets the version for a ser_ver record
-
- Args:
- svid (int): Record ID to update
- version (int): Version number to add
-
- Raises:
- ValueError: svid was not found
- """
- self.execute(
- 'UPDATE ser_ver SET version = ? WHERE id = ?', (version, svid))
- if self.rowcount() != 1:
- raise ValueError(f'No ser_ver updated (svid {svid})')
-
- def ser_ver_set_archive_tag(self, svid, tag):
- """Sets the archive tag for a ser_ver record
-
- Args:
- svid (int): Record ID to update
- tag (tag): Tag to add
-
- Raises:
- ValueError: svid was not found
- """
- self.execute(
- 'UPDATE ser_ver SET archive_tag = ? WHERE id = ?', (tag, svid))
- if self.rowcount() != 1:
- raise ValueError(f'No ser_ver updated (svid {svid})')
-
- def ser_ver_add(self, series_idnum, version, link=None):
- """Add a new ser_ver record
-
- Args:
- series_idnum (int): ID num of the series which is getting a new
- version
- version (int): Version number to add
- link (str): Patchwork link, or None if not known
-
- Return:
- int: ID num of the new ser_ver record
- """
- self.execute(
- 'INSERT INTO ser_ver (series_id, version, link) VALUES (?, ?, ?)',
- (series_idnum, version, link))
- return self.lastrowid()
-
- def ser_ver_get_for_series(self, series_idnum, version=None):
- """Get a list of ser_ver records for a given series ID
-
- Args:
- series_idnum (int): ID num of the series to search
- version (int): Version number to search for, or None for all
-
- Return:
- SER_VER: Requested information
-
- Raises:
- ValueError: There is no matching idnum/version
- """
- base = ('SELECT id, series_id, version, link, cover_id, '
- 'cover_num_comments, name, archive_tag FROM ser_ver '
- 'WHERE series_id = ?')
- if version:
- res = self.execute(base + ' AND version = ?',
- (series_idnum, version))
- else:
- res = self.execute(base, (series_idnum,))
- recs = res.fetchall()
- if not recs:
- raise ValueError(
- f'No matching series for id {series_idnum} version {version}')
- if version:
- return SerVer(*recs[0])
- return [SerVer(*x) for x in recs]
-
- def ser_ver_get_ids_for_series(self, series_idnum, version=None):
- """Get a list of ser_ver records for a given series ID
-
- Args:
- series_idnum (int): ID num of the series to search
- version (int): Version number to search for, or None for all
-
- Return:
- list of int: List of svids for the matching records
- """
- if version:
- res = self.execute(
- 'SELECT id FROM ser_ver WHERE series_id = ? AND version = ?',
- (series_idnum, version))
- else:
- res = self.execute(
- 'SELECT id FROM ser_ver WHERE series_id = ?', (series_idnum,))
- return list(res.fetchall()[0])
-
- def ser_ver_get_list(self):
- """Get a list of patchwork entries from the database
-
- Return:
- list of SER_VER
- """
- res = self.execute(
- 'SELECT id, series_id, version, link, cover_id, '
- 'cover_num_comments, name, archive_tag FROM ser_ver')
- items = res.fetchall()
- return [SerVer(*x) for x in items]
-
- def ser_ver_remove(self, series_idnum, version=None, remove_pcommits=True,
- remove_series=True):
- """Delete a ser_ver record
-
- Removes the record which has the given series ID num and version
-
- Args:
- series_idnum (int): ID num of the series
- version (int): Version number, or None to remove all versions
- remove_pcommits (bool): True to remove associated pcommits too
- remove_series (bool): True to remove the series if versions is None
- """
- if remove_pcommits:
- # Figure out svids to delete
- svids = self.ser_ver_get_ids_for_series(series_idnum, version)
-
- self.pcommit_delete_list(svids)
-
- if version:
- self.execute(
- 'DELETE FROM ser_ver WHERE series_id = ? AND version = ?',
- (series_idnum, version))
- else:
- self.execute(
- 'DELETE FROM ser_ver WHERE series_id = ?',
- (series_idnum,))
- if not version and remove_series:
- self.series_remove(series_idnum)
-
- # pcommit functions
-
- def pcommit_get_list(self, find_svid=None):
- """Get a dict of pcommits entries from the database
-
- Args:
- find_svid (int): If not None, finds the records associated with a
- particular series and version; otherwise returns all records
-
- Return:
- list of PCOMMIT: pcommit records
- """
- query = ('SELECT id, seq, subject, svid, change_id, state, patch_id, '
- 'num_comments FROM pcommit')
- if find_svid is not None:
- query += f' WHERE svid = {find_svid}'
- res = self.execute(query)
- return [Pcommit(*rec) for rec in res.fetchall()]
-
- def pcommit_add_list(self, svid, pcommits):
- """Add records to the pcommit table
-
- Args:
- svid (int): ser_ver ID num
- pcommits (list of PCOMMIT): Only seq, subject, change_id are
- uses; svid comes from the argument passed in and the others
- are assumed to be obtained from patchwork later
- """
- for pcm in pcommits:
- self.execute(
- 'INSERT INTO pcommit (svid, seq, subject, change_id) VALUES '
- '(?, ?, ?, ?)', (svid, pcm.seq, pcm.subject, pcm.change_id))
-
- def pcommit_delete(self, svid):
- """Delete pcommit records for a given ser_ver ID
-
- Args_:
- svid (int): ser_ver ID num of records to delete
- """
- self.execute('DELETE FROM pcommit WHERE svid = ?', (svid,))
-
- def pcommit_delete_list(self, svid_list):
- """Delete pcommit records for a given set of ser_ver IDs
-
- Args_:
- svid (list int): ser_ver ID nums of records to delete
- """
- vals = ', '.join([str(x) for x in svid_list])
- self.execute('DELETE FROM pcommit WHERE svid IN (?)', (vals,))
-
- def pcommit_update(self, pcm):
- """Update a pcommit record
-
- Args:
- pcm (PCOMMIT): Information to write; only the idnum, state,
- patch_id and num_comments are used
-
- Return:
- True if the data was written
- """
- self.execute(
- 'UPDATE pcommit SET '
- 'patch_id = ?, state = ?, num_comments = ? WHERE id = ?',
- (pcm.patch_id, pcm.state, pcm.num_comments, pcm.idnum))
- return self.rowcount() > 0
-
- # upstream functions
-
- def upstream_add(self, name, url):
- """Add a new upstream record
-
- Args:
- name (str): Name of the tree
- url (str): URL for the tree
-
- Raises:
- ValueError if the name already exists in the database
- """
- try:
- self.execute(
- 'INSERT INTO upstream (name, url) VALUES (?, ?)', (name, url))
- except sqlite3.IntegrityError as exc:
- if 'UNIQUE constraint failed: upstream.name' in str(exc):
- raise ValueError(f"Upstream '{name}' already exists") from exc
-
- def upstream_set_default(self, name):
- """Mark (only) the given upstream as the default
-
- Args:
- name (str): Name of the upstream remote to set as default, or None
-
- Raises:
- ValueError if more than one name matches (should not happen);
- database is rolled back
- """
- self.execute("UPDATE upstream SET is_default = 0")
- if name is not None:
- self.execute(
- 'UPDATE upstream SET is_default = 1 WHERE name = ?', (name,))
- if self.rowcount() != 1:
- self.rollback()
- raise ValueError(f"No such upstream '{name}'")
-
- def upstream_get_default(self):
- """Get the name of the default upstream
-
- Return:
- str: Default-upstream name, or None if there is no default
- """
- res = self.execute(
- "SELECT name FROM upstream WHERE is_default = 1")
- recs = res.fetchall()
- if len(recs) != 1:
- return None
- return recs[0][0]
-
- def upstream_delete(self, name):
- """Delete an upstream target
-
- Args:
- name (str): Name of the upstream remote to delete
-
- Raises:
- ValueError: Upstream does not exist (database is rolled back)
- """
- self.execute(f"DELETE FROM upstream WHERE name = '{name}'")
- if self.rowcount() != 1:
- self.rollback()
- raise ValueError(f"No such upstream '{name}'")
-
- def upstream_get_dict(self):
- """Get a list of upstream entries from the database
-
- Return:
- OrderedDict:
- key (str): upstream name
- value (str): url
- """
- res = self.execute('SELECT name, url, is_default FROM upstream')
- udict = OrderedDict()
- for name, url, is_default in res.fetchall():
- udict[name] = url, is_default
- return udict
-
- # settings functions
-
- def settings_update(self, name, proj_id, link_name):
- """Set the patchwork settings of the project
-
- Args:
- name (str): Name of the project to use in patchwork
- proj_id (int): Project ID for the project
- link_name (str): Link name for the project
- """
- self.execute('DELETE FROM settings')
- self.execute(
- 'INSERT INTO settings (name, proj_id, link_name) '
- 'VALUES (?, ?, ?)', (name, proj_id, link_name))
-
- def settings_get(self):
- """Get the patchwork settings of the project
-
- Returns:
- tuple or None if there are no settings:
- name (str): Project name, e.g. 'U-Boot'
- proj_id (int): Patchworks project ID for this project
- link_name (str): Patchwork's link-name for the project
- """
- res = self.execute("SELECT name, proj_id, link_name FROM settings")
- recs = res.fetchall()
- if len(recs) != 1:
- return None
- return recs[0]