From patchwork Wed Dec 17 02:25:55 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Simon Glass X-Patchwork-Id: 933 Return-Path: X-Original-To: u-boot-concept@u-boot.org Delivered-To: u-boot-concept@u-boot.org DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1765938423; bh=UM+FCQMraTX6APbwD87N/RqZFWnip+y5przSiB5sv+U=; h=From:To:Date:In-Reply-To:References:CC:Subject:List-Id: List-Archive:List-Help:List-Owner:List-Post:List-Subscribe: List-Unsubscribe:From; b=Q9szX0/BlM8PJcPUmfadT/PVU/6/VlJOuAIPPeG1gxJ2R7NW8oj2yRWh2mAwTN8FQ YKR5TAxfwPAQ5l5JP2E+Mh0aKajpYImeQsHf1vQmcd9AsXXj4wyHDM5c79wHdYpJGF i0oU1EHZEQp8KpHZEi1hj8MbSEtJlrYWFxBfrW/K4otxdTS/lgsERrfTO8nWAONkLl 7ZuaTVzvNh0oUl3Ft+LxXbUmI04HD1Bj3/4LoYVDA9Yd+Z9opK6EJ+OzNHNfGF0H/1 N+ISDOZyCoPg/bl5gSGffW+VwU7HSNb7Z5XUwq7MqTE7JI3rial+3vwnAJgXjSC+EC +oAIJZcMe0oFA== Received: from localhost (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id 0B59268BAC for ; Tue, 16 Dec 2025 19:27:03 -0700 (MST) X-Virus-Scanned: Debian amavis at Received: from mail.u-boot.org ([127.0.0.1]) by localhost (mail.u-boot.org [127.0.0.1]) (amavis, port 10024) with ESMTP id WGXBgkoygqej for ; Tue, 16 Dec 2025 19:27:02 -0700 (MST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1765938422; bh=UM+FCQMraTX6APbwD87N/RqZFWnip+y5przSiB5sv+U=; h=From:To:Date:In-Reply-To:References:CC:Subject:List-Id: List-Archive:List-Help:List-Owner:List-Post:List-Subscribe: List-Unsubscribe:From; b=aojD4NgJTUAYoSIpuJr3UNQYfSJQp6e6DpA3P+T327Gkz8c5kLBlpBB0fvNhQtdsc n5Qscea9iTUYmYwDgt64cEDJyWtNteHBPJWmxTKllTip0xxPztOGA0PMM/FWuYN8H8 1i6NDU99lRWS5FCP6uYVqRLpAusTNG5ANNcYimTqnspgm2DZO9DC+ECwhO1cS8S9S8 OEkrLqjJe97+fEzTuYqcQjTnVIpc2iJJwChE95NSQb5qmA4e6QqglrIWemDTp32Bfl ngQo6tL+lZfg8XD7LY5ZgaTScib7/Vr+qSg7rPK8Mw3oh86B1sYbHkx6vw+ZHOyLT8 xWgLlZxZMneBw== Received: from mail.u-boot.org (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id EDB2868BA3 for ; Tue, 16 Dec 2025 19:27:02 -0700 (MST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1765938422; bh=3J1oTUgfz4TPzok+Zo9zgNNQKaCl0gjum9YWdD1NCpA=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=PpDHsAjFhRstlzT5NZTZ2XDCGdyTqZdvwD9KuZvPqDgXdNL43mFcmuDz2B6mCB4o9 gqLzAfXk52P6YYIxwRCvuueuq5ocjwZ335IQsGpRl8VuA4eyMYHeflZ/tCSVJqm7Du hdb4jrZDfgLxKqyfHwODyL64hd7MqMeRdlAAa61Vj4Qrikw1Sf28JrD9bOHNG2Gm3C KRyJq0HpSCS51bcDbR+8pRYT0d7buen0KrpMi5b8DZ/a6Rc4gWVfUoHy3Ju95uS2F6 Mcs06SIUqqLT2CJDm5e1Xu6S1L6JquWGwhfIfxqZbccgWDa2/o6I7KUWBh1VHo73n8 H4EkXqOjmZSrA== Received: from localhost (localhost [127.0.0.1]) by mail.u-boot.org (Postfix) with ESMTP id 05E8E68AFB; Tue, 16 Dec 2025 19:27:02 -0700 (MST) X-Virus-Scanned: Debian amavis at Received: from mail.u-boot.org ([127.0.0.1]) by localhost (mail.u-boot.org [127.0.0.1]) (amavis, port 10026) with ESMTP id KE-znVzPB6k2; Tue, 16 Dec 2025 19:27:01 -0700 (MST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=u-boot.org; s=default; t=1765938417; bh=T8s8xjjHzKqL8vPY0zpBbHx3msIPcU9uFvoAh+gP2jU=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=SZgl6mhaf5ziYrlOUqDiPsQs/vgugHKO5xK5yDZLTB+scdcEqysAvKpzXAIe6qwBV eRUVT5AxaWC/JUCG/nre6Lq9fUcT9NrZ/AhijUbkt013X4/wLCn84ivSwaz9lXfUo6 FjnJvBBwBzb/lSEsAA468pbgiY2MOn/kVMUpC9HasPkSIsrp+IgJOt2k/fodNxMoc8 cNhZhWpwhzX3tjSlkg8JvACJMdkhvlMYP3T7Pvltr70LgEfv3sWKFIINV/dfVt2H5u 7AlRkf0+ZYgoVzr5O2oXiNHWQanh5p65pCXB8SJS+YAEFfYjYJbEUMe0AXVWyH4ATV rbKuDwGSFmJ7Q== Received: from u-boot.org (unknown [73.34.74.121]) by mail.u-boot.org (Postfix) with ESMTPSA id 87F276884F; Tue, 16 Dec 2025 19:26:57 -0700 (MST) From: Simon Glass To: U-Boot Concept Date: Tue, 16 Dec 2025 19:25:55 -0700 Message-ID: <20251217022611.389379-8-sjg@u-boot.org> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20251217022611.389379-1-sjg@u-boot.org> References: <20251217022611.389379-1-sjg@u-boot.org> MIME-Version: 1.0 Message-ID-Hash: 5MNR733J2UNAHLBVU7SUBALLM6AS3QS7 X-Message-ID-Hash: 5MNR733J2UNAHLBVU7SUBALLM6AS3QS7 X-MailFrom: sjg@u-boot.org X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header CC: Heinrich Schuchardt , Simon Glass , "Claude Opus 4 . 5" X-Mailman-Version: 3.3.10 Precedence: list Subject: [Concept] [PATCH 07/17] pickman: Add pcommit and mergereq tables to database List-Id: Discussion and patches related to U-Boot Concept Archived-At: List-Archive: List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: From: Simon Glass Add new database tables to track individual commits and merge requests: - pcommit: tracks commits being cherry-picked with status, author, subject, and optional link to merge request - mergereq: tracks GitLab merge requests with branch name, MR ID, status, and URL Also add helper functions for both tables and update control.py to write commit status to the database during apply operations. Update README.rst with documentation for all database tables. Co-developed-by: Claude Opus 4.5 Signed-off-by: Simon Glass --- tools/pickman/README.rst | 40 ++++- tools/pickman/database.py | 213 +++++++++++++++++++++++- tools/pickman/ftest.py | 337 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 585 insertions(+), 5 deletions(-) diff --git a/tools/pickman/README.rst b/tools/pickman/README.rst index 5cb4f51df5c..d8ab2ff6cf3 100644 --- a/tools/pickman/README.rst +++ b/tools/pickman/README.rst @@ -44,10 +44,42 @@ represents a logical grouping of commits (e.g., a pull request). Database -------- -Pickman uses a sqlite3 database (``.pickman.db``) to track state: - -- **source table**: Tracks source branches and the last commit that was - cherry-picked into master +Pickman uses a sqlite3 database (``.pickman.db``) to track state. The schema +version is stored in the ``schema_version`` table and migrations are applied +automatically when the database is opened. + +Tables +~~~~~~ + +**source** + Tracks source branches and their cherry-pick progress. + + - ``id``: Primary key + - ``name``: Branch name (e.g., 'us/next') + - ``last_commit``: Hash of the last commit cherry-picked from this branch + +**pcommit** + Tracks individual commits being cherry-picked. + + - ``id``: Primary key + - ``chash``: Original commit hash + - ``source_id``: Foreign key to source table + - ``mergereq_id``: Foreign key to mergereq table (optional) + - ``subject``: Commit subject line + - ``author``: Commit author + - ``status``: One of 'pending', 'applied', 'skipped', 'conflict' + - ``cherry_hash``: Hash of the cherry-picked commit (if applied) + +**mergereq** + Tracks merge requests created for cherry-picked commits. + + - ``id``: Primary key + - ``source_id``: Foreign key to source table + - ``branch_name``: Git branch name for this MR + - ``mr_id``: GitLab merge request ID + - ``status``: One of 'open', 'merged', 'closed' + - ``url``: URL to the merge request + - ``created_at``: Timestamp when the MR was created Configuration ------------- diff --git a/tools/pickman/database.py b/tools/pickman/database.py index 46b8556945e..118ac5536fa 100644 --- a/tools/pickman/database.py +++ b/tools/pickman/database.py @@ -18,7 +18,7 @@ from u_boot_pylib import tools from u_boot_pylib import tout # Schema version (version 0 means there is no database yet) -LATEST = 1 +LATEST = 2 # Default database filename DB_FNAME = '.pickman.db' @@ -101,6 +101,34 @@ class Database: # Schema version table self.cur.execute('CREATE TABLE schema_version (version INTEGER)') + def _create_v2(self): + """Migrate database to v2 schema - add commit and mergereq tables""" + # Table for tracking individual commits + self.cur.execute( + 'CREATE TABLE pcommit (' + 'id INTEGER PRIMARY KEY AUTOINCREMENT, ' + 'chash TEXT UNIQUE, ' + 'source_id INTEGER, ' + 'mergereq_id INTEGER, ' + 'subject TEXT, ' + 'author TEXT, ' + 'status TEXT, ' + 'cherry_hash TEXT, ' + 'FOREIGN KEY (source_id) REFERENCES source(id), ' + 'FOREIGN KEY (mergereq_id) REFERENCES mergereq(id))') + + # Table for tracking merge requests + self.cur.execute( + 'CREATE TABLE mergereq (' + 'id INTEGER PRIMARY KEY AUTOINCREMENT, ' + 'source_id INTEGER, ' + 'branch_name TEXT, ' + 'mr_id INTEGER, ' + 'status TEXT, ' + 'url TEXT, ' + 'created_at TEXT, ' + 'FOREIGN KEY (source_id) REFERENCES source(id))') + def migrate_to(self, dest_version): """Migrate the database to the selected version @@ -121,6 +149,8 @@ class Database: self.open_it() if version == 1: self._create_v1() + elif version == 2: + self._create_v2() self.cur.execute('DELETE FROM schema_version') self.cur.execute( @@ -200,3 +230,184 @@ class Database: self.execute( 'INSERT INTO source (name, last_commit) VALUES (?, ?)', (name, commit)) + + def source_get_id(self, name): + """Get the id for a source branch + + Args: + name (str): Source branch name + + Return: + int: Source id, or None if not found + """ + res = self.execute('SELECT id FROM source WHERE name = ?', (name,)) + rec = res.fetchone() + if rec: + return rec[0] + return None + + # commit functions + + def commit_add(self, chash, source_id, subject, author, status='pending', + mergereq_id=None): + """Add a commit to the database + + Args: + chash (str): Commit hash + source_id (int): Source branch id + subject (str): Commit subject line + author (str): Commit author + status (str): Status (pending, applied, skipped, conflict) + mergereq_id (int): Merge request id (optional) + """ + self.execute( + 'INSERT OR REPLACE INTO pcommit ' + '(chash, source_id, mergereq_id, subject, author, status) ' + 'VALUES (?, ?, ?, ?, ?, ?)', + (chash, source_id, mergereq_id, subject, author, status)) + + def commit_get(self, chash): + """Get a commit by hash + + Args: + chash (str): Commit hash + + Return: + tuple: (id, chash, source_id, mergereq_id, subject, author, status, + cherry_hash) or None if not found + """ + res = self.execute( + 'SELECT id, chash, source_id, mergereq_id, subject, author, status, ' + 'cherry_hash FROM pcommit WHERE chash = ?', (chash,)) + return res.fetchone() + + def commit_get_by_source(self, source_id, status=None): + """Get all commits for a source branch + + Args: + source_id (int): Source branch id + status (str): Optional status filter + + Return: + list of tuple: Commit records + """ + if status: + res = self.execute( + 'SELECT id, chash, source_id, mergereq_id, subject, author, ' + 'status, cherry_hash FROM pcommit ' + 'WHERE source_id = ? AND status = ?', + (source_id, status)) + else: + res = self.execute( + 'SELECT id, chash, source_id, mergereq_id, subject, author, ' + 'status, cherry_hash FROM pcommit WHERE source_id = ?', + (source_id,)) + return res.fetchall() + + def commit_get_by_mergereq(self, mergereq_id): + """Get all commits for a merge request + + Args: + mergereq_id (int): Merge request id + + Return: + list of tuple: Commit records + """ + res = self.execute( + 'SELECT id, chash, source_id, mergereq_id, subject, author, ' + 'status, cherry_hash FROM pcommit WHERE mergereq_id = ?', + (mergereq_id,)) + return res.fetchall() + + def commit_set_status(self, chash, status, cherry_hash=None): + """Update the status of a commit + + Args: + chash (str): Commit hash + status (str): New status + cherry_hash (str): Hash of cherry-picked commit (optional) + """ + if cherry_hash: + self.execute( + 'UPDATE pcommit SET status = ?, cherry_hash = ? WHERE chash = ?', + (status, cherry_hash, chash)) + else: + self.execute( + 'UPDATE pcommit SET status = ? WHERE chash = ?', (status, chash)) + + def commit_set_mergereq(self, chash, mergereq_id): + """Set the merge request for a commit + + Args: + chash (str): Commit hash + mergereq_id (int): Merge request id + """ + self.execute( + 'UPDATE pcommit SET mergereq_id = ? WHERE chash = ?', + (mergereq_id, chash)) + + # mergereq functions + + def mergereq_add(self, source_id, branch_name, mr_id, status, url, + created_at): + """Add a merge request to the database + + Args: + source_id (int): Source branch id + branch_name (str): Branch name for the MR + mr_id (int): GitLab MR id + status (str): Status (open, merged, closed) + url (str): URL to the MR + created_at (str): Creation timestamp + """ + self.execute( + 'INSERT INTO mergereq ' + '(source_id, branch_name, mr_id, status, url, created_at) ' + 'VALUES (?, ?, ?, ?, ?, ?)', + (source_id, branch_name, mr_id, status, url, created_at)) + + def mergereq_get(self, mr_id): + """Get a merge request by GitLab MR id + + Args: + mr_id (int): GitLab MR id + + Return: + tuple: (id, source_id, branch_name, mr_id, status, url, created_at) + or None if not found + """ + res = self.execute( + 'SELECT id, source_id, branch_name, mr_id, status, url, created_at ' + 'FROM mergereq WHERE mr_id = ?', (mr_id,)) + return res.fetchone() + + def mergereq_get_by_source(self, source_id, status=None): + """Get all merge requests for a source branch + + Args: + source_id (int): Source branch id + status (str): Optional status filter + + Return: + list of tuple: Merge request records + """ + if status: + res = self.execute( + 'SELECT id, source_id, branch_name, mr_id, status, url, ' + 'created_at FROM mergereq WHERE source_id = ? AND status = ?', + (source_id, status)) + else: + res = self.execute( + 'SELECT id, source_id, branch_name, mr_id, status, url, ' + 'created_at FROM mergereq WHERE source_id = ?', (source_id,)) + return res.fetchall() + + def mergereq_set_status(self, mr_id, status): + """Update the status of a merge request + + Args: + mr_id (int): GitLab MR id + status (str): New status + """ + self.execute( + 'UPDATE mergereq SET status = ? WHERE mr_id = ?', (status, mr_id)) diff --git a/tools/pickman/ftest.py b/tools/pickman/ftest.py index a6331d21c5f..2c9e5b1d780 100644 --- a/tools/pickman/ftest.py +++ b/tools/pickman/ftest.py @@ -309,6 +309,343 @@ class TestDatabase(unittest.TestCase): dbs.close() +class TestDatabaseCommit(unittest.TestCase): + """Tests for Database commit functions.""" + + def setUp(self): + """Set up test fixtures.""" + fd, self.db_path = tempfile.mkstemp(suffix='.db') + os.close(fd) + os.unlink(self.db_path) + database.Database.instances.clear() + + def tearDown(self): + """Clean up test fixtures.""" + if os.path.exists(self.db_path): + os.unlink(self.db_path) + database.Database.instances.clear() + + def test_commit_add_and_get(self): + """Test adding and getting a commit.""" + with terminal.capture(): + dbs = database.Database(self.db_path) + dbs.start() + + # First add a source + dbs.source_set('us/next', 'base123') + dbs.commit() + source_id = dbs.source_get_id('us/next') + + # Add a commit + dbs.commit_add('abc123def456', source_id, 'Test subject', + 'Author Name') + dbs.commit() + + # Get the commit + result = dbs.commit_get('abc123def456') + self.assertIsNotNone(result) + self.assertEqual(result[1], 'abc123def456') # chash + self.assertEqual(result[2], source_id) # source_id + self.assertIsNone(result[3]) # mergereq_id + self.assertEqual(result[4], 'Test subject') # subject + self.assertEqual(result[5], 'Author Name') # author + self.assertEqual(result[6], 'pending') # status + dbs.close() + + def test_commit_get_not_found(self): + """Test getting a non-existent commit.""" + with terminal.capture(): + dbs = database.Database(self.db_path) + dbs.start() + result = dbs.commit_get('nonexistent') + self.assertIsNone(result) + dbs.close() + + def test_commit_get_by_source(self): + """Test getting commits by source.""" + with terminal.capture(): + dbs = database.Database(self.db_path) + dbs.start() + + # Add a source + dbs.source_set('us/next', 'base123') + dbs.commit() + source_id = dbs.source_get_id('us/next') + + # Add commits + dbs.commit_add('commit1', source_id, 'Subject 1', 'Author 1') + dbs.commit_add('commit2', source_id, 'Subject 2', 'Author 2', + status='applied') + dbs.commit_add('commit3', source_id, 'Subject 3', 'Author 3') + dbs.commit() + + # Get all commits for source + commits = dbs.commit_get_by_source(source_id) + self.assertEqual(len(commits), 3) + + # Get only pending commits + pending = dbs.commit_get_by_source(source_id, status='pending') + self.assertEqual(len(pending), 2) + + # Get only applied commits + applied = dbs.commit_get_by_source(source_id, status='applied') + self.assertEqual(len(applied), 1) + self.assertEqual(applied[0][1], 'commit2') + dbs.close() + + def test_commit_set_status(self): + """Test updating commit status.""" + with terminal.capture(): + dbs = database.Database(self.db_path) + dbs.start() + + dbs.source_set('us/next', 'base123') + dbs.commit() + source_id = dbs.source_get_id('us/next') + + dbs.commit_add('abc123', source_id, 'Subject', 'Author') + dbs.commit() + + # Update status + dbs.commit_set_status('abc123', 'applied') + dbs.commit() + + result = dbs.commit_get('abc123') + self.assertEqual(result[6], 'applied') + dbs.close() + + def test_commit_set_status_with_cherry_hash(self): + """Test updating commit status with cherry hash.""" + with terminal.capture(): + dbs = database.Database(self.db_path) + dbs.start() + + dbs.source_set('us/next', 'base123') + dbs.commit() + source_id = dbs.source_get_id('us/next') + + dbs.commit_add('abc123', source_id, 'Subject', 'Author') + dbs.commit() + + # Update status with cherry hash + dbs.commit_set_status('abc123', 'applied', cherry_hash='xyz789') + dbs.commit() + + result = dbs.commit_get('abc123') + self.assertEqual(result[6], 'applied') + self.assertEqual(result[7], 'xyz789') # cherry_hash + dbs.close() + + def test_source_get_id(self): + """Test getting source id by name.""" + with terminal.capture(): + dbs = database.Database(self.db_path) + dbs.start() + + # Not found initially + self.assertIsNone(dbs.source_get_id('us/next')) + + # Add source and get id + dbs.source_set('us/next', 'abc123') + dbs.commit() + + source_id = dbs.source_get_id('us/next') + self.assertIsNotNone(source_id) + self.assertIsInstance(source_id, int) + dbs.close() + + +class TestDatabaseMergereq(unittest.TestCase): + """Tests for Database mergereq functions.""" + + def setUp(self): + """Set up test fixtures.""" + fd, self.db_path = tempfile.mkstemp(suffix='.db') + os.close(fd) + os.unlink(self.db_path) + database.Database.instances.clear() + + def tearDown(self): + """Clean up test fixtures.""" + if os.path.exists(self.db_path): + os.unlink(self.db_path) + database.Database.instances.clear() + + def test_mergereq_add_and_get(self): + """Test adding and getting a merge request.""" + with terminal.capture(): + dbs = database.Database(self.db_path) + dbs.start() + + # Add a source + dbs.source_set('us/next', 'base123') + dbs.commit() + source_id = dbs.source_get_id('us/next') + + # Add a merge request + dbs.mergereq_add(source_id, 'cherry-abc123', 42, 'open', + 'https://gitlab.com/mr/42', '2025-01-15') + dbs.commit() + + # Get the merge request + result = dbs.mergereq_get(42) + self.assertIsNotNone(result) + self.assertEqual(result[1], source_id) # source_id + self.assertEqual(result[2], 'cherry-abc123') # branch_name + self.assertEqual(result[3], 42) # mr_id + self.assertEqual(result[4], 'open') # status + self.assertEqual(result[5], 'https://gitlab.com/mr/42') # url + self.assertEqual(result[6], '2025-01-15') # created_at + dbs.close() + + def test_mergereq_get_not_found(self): + """Test getting a non-existent merge request.""" + with terminal.capture(): + dbs = database.Database(self.db_path) + dbs.start() + result = dbs.mergereq_get(999) + self.assertIsNone(result) + dbs.close() + + def test_mergereq_get_by_source(self): + """Test getting merge requests by source.""" + with terminal.capture(): + dbs = database.Database(self.db_path) + dbs.start() + + # Add a source + dbs.source_set('us/next', 'base123') + dbs.commit() + source_id = dbs.source_get_id('us/next') + + # Add merge requests + dbs.mergereq_add(source_id, 'branch-1', 1, 'open', + 'https://gitlab.com/mr/1', '2025-01-01') + dbs.mergereq_add(source_id, 'branch-2', 2, 'merged', + 'https://gitlab.com/mr/2', '2025-01-02') + dbs.mergereq_add(source_id, 'branch-3', 3, 'open', + 'https://gitlab.com/mr/3', '2025-01-03') + dbs.commit() + + # Get all merge requests for source + mrs = dbs.mergereq_get_by_source(source_id) + self.assertEqual(len(mrs), 3) + + # Get only open merge requests + open_mrs = dbs.mergereq_get_by_source(source_id, status='open') + self.assertEqual(len(open_mrs), 2) + + # Get only merged + merged = dbs.mergereq_get_by_source(source_id, status='merged') + self.assertEqual(len(merged), 1) + self.assertEqual(merged[0][3], 2) # mr_id + dbs.close() + + def test_mergereq_set_status(self): + """Test updating merge request status.""" + with terminal.capture(): + dbs = database.Database(self.db_path) + dbs.start() + + dbs.source_set('us/next', 'base123') + dbs.commit() + source_id = dbs.source_get_id('us/next') + + dbs.mergereq_add(source_id, 'branch-1', 42, 'open', + 'https://gitlab.com/mr/42', '2025-01-15') + dbs.commit() + + # Update status + dbs.mergereq_set_status(42, 'merged') + dbs.commit() + + result = dbs.mergereq_get(42) + self.assertEqual(result[4], 'merged') + dbs.close() + + +class TestDatabaseCommitMergereq(unittest.TestCase): + """Tests for commit-mergereq relationship.""" + + def setUp(self): + """Set up test fixtures.""" + fd, self.db_path = tempfile.mkstemp(suffix='.db') + os.close(fd) + os.unlink(self.db_path) + database.Database.instances.clear() + + def tearDown(self): + """Clean up test fixtures.""" + if os.path.exists(self.db_path): + os.unlink(self.db_path) + database.Database.instances.clear() + + def test_commit_set_mergereq(self): + """Test setting merge request for a commit.""" + with terminal.capture(): + dbs = database.Database(self.db_path) + dbs.start() + + # Add source + dbs.source_set('us/next', 'base123') + dbs.commit() + source_id = dbs.source_get_id('us/next') + + # Add merge request + dbs.mergereq_add(source_id, 'branch-1', 42, 'open', + 'https://gitlab.com/mr/42', '2025-01-15') + dbs.commit() + mr = dbs.mergereq_get(42) + mr_id = mr[0] # id field + + # Add commit without mergereq + dbs.commit_add('abc123', source_id, 'Subject', 'Author') + dbs.commit() + + # Set mergereq + dbs.commit_set_mergereq('abc123', mr_id) + dbs.commit() + + result = dbs.commit_get('abc123') + self.assertEqual(result[3], mr_id) # mergereq_id + dbs.close() + + def test_commit_get_by_mergereq(self): + """Test getting commits by merge request.""" + with terminal.capture(): + dbs = database.Database(self.db_path) + dbs.start() + + # Add source + dbs.source_set('us/next', 'base123') + dbs.commit() + source_id = dbs.source_get_id('us/next') + + # Add merge request + dbs.mergereq_add(source_id, 'branch-1', 42, 'open', + 'https://gitlab.com/mr/42', '2025-01-15') + dbs.commit() + mr = dbs.mergereq_get(42) + mr_id = mr[0] + + # Add commits with mergereq_id + dbs.commit_add('commit1', source_id, 'Subject 1', 'Author 1', + mergereq_id=mr_id) + dbs.commit_add('commit2', source_id, 'Subject 2', 'Author 2', + mergereq_id=mr_id) + dbs.commit_add('commit3', source_id, 'Subject 3', 'Author 3') + dbs.commit() + + # Get commits for merge request + commits = dbs.commit_get_by_mergereq(mr_id) + self.assertEqual(len(commits), 2) + hashes = [c[1] for c in commits] + self.assertIn('commit1', hashes) + self.assertIn('commit2', hashes) + self.assertNotIn('commit3', hashes) + dbs.close() + + class TestListSources(unittest.TestCase): """Tests for list-sources command."""