git-gerrit 3.0.0__tar.gz → 3.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. {git_gerrit-3.0.0/git_gerrit.egg-info → git_gerrit-3.2.0}/PKG-INFO +36 -11
  2. {git_gerrit-3.0.0 → git_gerrit-3.2.0}/README.rst +35 -10
  3. git_gerrit-3.2.0/git_gerrit/__version__.py +1 -0
  4. {git_gerrit-3.0.0 → git_gerrit-3.2.0}/git_gerrit/core.py +16 -3
  5. {git_gerrit-3.0.0 → git_gerrit-3.2.0}/git_gerrit/db.py +188 -45
  6. {git_gerrit-3.0.0 → git_gerrit-3.2.0}/git_gerrit/git.py +6 -3
  7. {git_gerrit-3.0.0 → git_gerrit-3.2.0/git_gerrit.egg-info}/PKG-INFO +36 -11
  8. {git_gerrit-3.0.0 → git_gerrit-3.2.0}/tests/test_core.py +33 -2
  9. git_gerrit-3.2.0/tests/test_db.py +205 -0
  10. {git_gerrit-3.0.0 → git_gerrit-3.2.0}/tests/test_git.py +3 -1
  11. git_gerrit-3.0.0/git_gerrit/__version__.py +0 -1
  12. git_gerrit-3.0.0/tests/test_db.py +0 -121
  13. {git_gerrit-3.0.0 → git_gerrit-3.2.0}/LICENSE.txt +0 -0
  14. {git_gerrit-3.0.0 → git_gerrit-3.2.0}/MANIFEST.in +0 -0
  15. {git_gerrit-3.0.0 → git_gerrit-3.2.0}/git_gerrit/__init__.py +0 -0
  16. {git_gerrit-3.0.0 → git_gerrit-3.2.0}/git_gerrit/cli.py +0 -0
  17. {git_gerrit-3.0.0 → git_gerrit-3.2.0}/git_gerrit/error.py +0 -0
  18. {git_gerrit-3.0.0 → git_gerrit-3.2.0}/git_gerrit/spinner.py +0 -0
  19. {git_gerrit-3.0.0 → git_gerrit-3.2.0}/git_gerrit.egg-info/SOURCES.txt +0 -0
  20. {git_gerrit-3.0.0 → git_gerrit-3.2.0}/git_gerrit.egg-info/dependency_links.txt +0 -0
  21. {git_gerrit-3.0.0 → git_gerrit-3.2.0}/git_gerrit.egg-info/entry_points.txt +0 -0
  22. {git_gerrit-3.0.0 → git_gerrit-3.2.0}/git_gerrit.egg-info/requires.txt +0 -0
  23. {git_gerrit-3.0.0 → git_gerrit-3.2.0}/git_gerrit.egg-info/top_level.txt +0 -0
  24. {git_gerrit-3.0.0 → git_gerrit-3.2.0}/pyproject.toml +0 -0
  25. {git_gerrit-3.0.0 → git_gerrit-3.2.0}/setup.cfg +0 -0
  26. {git_gerrit-3.0.0 → git_gerrit-3.2.0}/setup.py +0 -0
  27. {git_gerrit-3.0.0 → git_gerrit-3.2.0}/tests/test_cli.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: git_gerrit
3
- Version: 3.0.0
3
+ Version: 3.2.0
4
4
  Summary: Gerrit review system command line tools.
5
5
  Home-page: https://github.com/meffie/git-gerrit
6
6
  Author: Michael Meffie
@@ -39,7 +39,7 @@ with the **pygerrit2** package to access the Gerrit REST API.
39
39
  Commands
40
40
  ========
41
41
 
42
- .. code-block:: console
42
+ **git-gerrit** commands::
43
43
 
44
44
  git gerrit-checkout Fetch then checkout by gerrit number.
45
45
  git gerrit-cherry-pick Cherry pick from upstream branch by gerrit number to make a new gerrit.
@@ -57,16 +57,32 @@ Installation
57
57
  ============
58
58
 
59
59
  .. _pipx: https://pipx.pypa.io/stable/
60
+ .. _uv: https://github.com/astral-sh/uv
61
+
62
+ The recommended way to install **git-gerrit** is with `uv`_ or `pipx`_.
63
+
64
+ To install with ``uv``, run the following as a regular user:
60
65
 
61
- Install with `pipx`_::
66
+ .. code-block:: console
67
+
68
+ uv tool install git-gerrit
69
+ uv tool update-shell
62
70
 
63
- $ pipx install git-gerrit
71
+ The ``uv tool update-shell`` command adds the installed commands to your
72
+ shell's PATH. Restart your shell after running ``update-shell``.
64
73
 
65
- Be sure your ``PATH`` has been updated to run **pipx** installed commands::
74
+ To install with ``pipx_``, run the following as a regular user:
66
75
 
67
- $ pipx ensurepath
76
+ .. code-block:: console
68
77
 
69
- If **pipx** is not available, then install in a project local virtualenv::
78
+ pipx install git-gerrit
79
+ pipx ensurepath
80
+
81
+ The ``pipx ensurepath`` command adds the installed commands to your shell's
82
+ PATH. Restart you shell after running ``ensurepath``.
83
+
84
+ Alternatively, you can install **git-gerrit** directly into a local virtual
85
+ environment with ``pip``::
70
86
 
71
87
  $ cd <my-project-directory>
72
88
  $ python3 -m venv .venv
@@ -74,7 +90,6 @@ If **pipx** is not available, then install in a project local virtualenv::
74
90
  (.venv) $ pip install -U pip setuptools
75
91
  (.venv) $ pip install git-gerrit
76
92
 
77
-
78
93
  Setup
79
94
  =====
80
95
 
@@ -101,6 +116,16 @@ Setup a local OpenAFS git repo::
101
116
  $ git config --local gerrit.project openafs
102
117
  $ git gerrit-install-hooks
103
118
 
119
+ Fetch all of the change commits from the upstream gerrit system to your local
120
+ repository and create the local sqlite database. This is a heavy weight operation
121
+ and is only needed for **git gerrit-log** and **git gerrit-number**::
122
+
123
+ $ git gerrit-sync
124
+ ✔ Fetching changes from https://gerrit.example.com/example
125
+ ✔ Updating local database
126
+ ✔ Scanning commit messages
127
+ Done.
128
+
104
129
  Find open gerrits on the master branch::
105
130
 
106
131
  $ git gerrit-query is:open branch:master
@@ -163,14 +188,14 @@ Cherry-pick a gerrit onto the current branch::
163
188
 
164
189
  Show gerrit numbers in the checked out branch in the local git repo::
165
190
 
166
- $ git gerrit-log
191
+ $ git gerrit-log -n3
167
192
  12958 f47cb2d Suppress statement not reached warnings under Solaris Studio
168
193
  12957 306f0f3 afs: squash empty declaration warning
169
194
  12955 e006609 libafs: git ignore build artifacts on Solaris
170
195
 
171
196
  Show gerrit numbers by a revision in the local git repo::
172
197
 
173
- $ git gerrit-log openafs-stable-1_8_0
198
+ $ git gerrit-log -n3 openafs-stable-1_8_0
174
199
  12953 a08327f Update NEWS for 1.8.0 final release
175
200
  12938 acb0e84 afs_pioctl: avoid -Wpointer-sign
176
201
  12950 b73863b LINUX: fix RedHat 7.5 ENOTDIR issues
@@ -184,7 +209,7 @@ Show gerrit numbers by a range of revisions in the local git repo::
184
209
 
185
210
  Show just the gerrit numbers and subjects in the local git repo::
186
211
 
187
- $ git gerrit-log --format='{number}: {subject}'
212
+ $ git gerrit-log -n3 --format='{number}: {subject}'
188
213
  12958: Suppress statement not reached warnings under Solaris Studio
189
214
  12957: afs: squash empty declaration warning
190
215
  12955: libafs: git ignore build artifacts on Solaris
@@ -12,7 +12,7 @@ with the **pygerrit2** package to access the Gerrit REST API.
12
12
  Commands
13
13
  ========
14
14
 
15
- .. code-block:: console
15
+ **git-gerrit** commands::
16
16
 
17
17
  git gerrit-checkout Fetch then checkout by gerrit number.
18
18
  git gerrit-cherry-pick Cherry pick from upstream branch by gerrit number to make a new gerrit.
@@ -30,16 +30,32 @@ Installation
30
30
  ============
31
31
 
32
32
  .. _pipx: https://pipx.pypa.io/stable/
33
+ .. _uv: https://github.com/astral-sh/uv
34
+
35
+ The recommended way to install **git-gerrit** is with `uv`_ or `pipx`_.
36
+
37
+ To install with ``uv``, run the following as a regular user:
33
38
 
34
- Install with `pipx`_::
39
+ .. code-block:: console
40
+
41
+ uv tool install git-gerrit
42
+ uv tool update-shell
35
43
 
36
- $ pipx install git-gerrit
44
+ The ``uv tool update-shell`` command adds the installed commands to your
45
+ shell's PATH. Restart your shell after running ``update-shell``.
37
46
 
38
- Be sure your ``PATH`` has been updated to run **pipx** installed commands::
47
+ To install with ``pipx_``, run the following as a regular user:
39
48
 
40
- $ pipx ensurepath
49
+ .. code-block:: console
41
50
 
42
- If **pipx** is not available, then install in a project local virtualenv::
51
+ pipx install git-gerrit
52
+ pipx ensurepath
53
+
54
+ The ``pipx ensurepath`` command adds the installed commands to your shell's
55
+ PATH. Restart you shell after running ``ensurepath``.
56
+
57
+ Alternatively, you can install **git-gerrit** directly into a local virtual
58
+ environment with ``pip``::
43
59
 
44
60
  $ cd <my-project-directory>
45
61
  $ python3 -m venv .venv
@@ -47,7 +63,6 @@ If **pipx** is not available, then install in a project local virtualenv::
47
63
  (.venv) $ pip install -U pip setuptools
48
64
  (.venv) $ pip install git-gerrit
49
65
 
50
-
51
66
  Setup
52
67
  =====
53
68
 
@@ -74,6 +89,16 @@ Setup a local OpenAFS git repo::
74
89
  $ git config --local gerrit.project openafs
75
90
  $ git gerrit-install-hooks
76
91
 
92
+ Fetch all of the change commits from the upstream gerrit system to your local
93
+ repository and create the local sqlite database. This is a heavy weight operation
94
+ and is only needed for **git gerrit-log** and **git gerrit-number**::
95
+
96
+ $ git gerrit-sync
97
+ ✔ Fetching changes from https://gerrit.example.com/example
98
+ ✔ Updating local database
99
+ ✔ Scanning commit messages
100
+ Done.
101
+
77
102
  Find open gerrits on the master branch::
78
103
 
79
104
  $ git gerrit-query is:open branch:master
@@ -136,14 +161,14 @@ Cherry-pick a gerrit onto the current branch::
136
161
 
137
162
  Show gerrit numbers in the checked out branch in the local git repo::
138
163
 
139
- $ git gerrit-log
164
+ $ git gerrit-log -n3
140
165
  12958 f47cb2d Suppress statement not reached warnings under Solaris Studio
141
166
  12957 306f0f3 afs: squash empty declaration warning
142
167
  12955 e006609 libafs: git ignore build artifacts on Solaris
143
168
 
144
169
  Show gerrit numbers by a revision in the local git repo::
145
170
 
146
- $ git gerrit-log openafs-stable-1_8_0
171
+ $ git gerrit-log -n3 openafs-stable-1_8_0
147
172
  12953 a08327f Update NEWS for 1.8.0 final release
148
173
  12938 acb0e84 afs_pioctl: avoid -Wpointer-sign
149
174
  12950 b73863b LINUX: fix RedHat 7.5 ENOTDIR issues
@@ -157,7 +182,7 @@ Show gerrit numbers by a range of revisions in the local git repo::
157
182
 
158
183
  Show just the gerrit numbers and subjects in the local git repo::
159
184
 
160
- $ git gerrit-log --format='{number}: {subject}'
185
+ $ git gerrit-log -n3 --format='{number}: {subject}'
161
186
  12958: Suppress statement not reached warnings under Solaris Studio
162
187
  12957: afs: squash empty declaration warning
163
188
  12955: libafs: git ignore build artifacts on Solaris
@@ -0,0 +1 @@
1
+ VERSION = "3.2.0"
@@ -492,25 +492,38 @@ def sync(limit=None):
492
492
  parts = refname.split("/")
493
493
  number = int(parts[3])
494
494
  patchset = int(parts[4])
495
- db.add_change(number, patchset, commit_id)
495
+ db.add_patchset(number, patchset, commit_id)
496
496
  spinner.spin()
497
497
 
498
498
  # It is not practical to read every commit message, and normally, we only
499
499
  # care about the current patchsets, so scan just the current patchsets
500
500
  # (that is the max patchset number of each change number). Also, limit the
501
- # number of changes to be scanned, since we normally care about jus the
502
- # most recent numbers. This amoritizes the scanning, so the first
501
+ # number of changes to be scanned, since we normally care about just the
502
+ # most recent numbers. This amortizes the scanning, so the first
503
503
  # git-gerrit-sync will scan a reasonable number of changes, and later syncs
504
504
  # will process older changes.
505
+ no_change_id = 0
505
506
  with Spinner("Scanning commit messages") as spinner:
506
507
  with GitGerritDB() as db:
507
508
  for c in db.get_current_patchsets(limit=limit):
508
509
  if c['flags'] != 1:
510
+ number = c['number']
511
+ current_patchset = c['current_patchset']
509
512
  commit_id = c['commit_id']
510
513
  change_id = git.change_id(commit_id)
514
+ if change_id is None:
515
+ # Some early changes were merged before gerrit enforced
516
+ # a Change-Id trailer. Record the commit, but skip the
517
+ # gerrit_changes row, which is keyed by Change-Id.
518
+ no_change_id += 1
519
+ else:
520
+ db.record_change(number, current_patchset, change_id)
521
+
511
522
  picked_from = git.cherry_picked_from(commit_id)
512
523
  db.update_commit(commit_id, change_id, picked_from, 1)
513
524
  spinner.spin()
514
525
 
526
+ if no_change_id:
527
+ print(f"Skipped {no_change_id} change(s) with no Change-Id.")
515
528
  print("Done.")
516
529
  return 0
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2025 Sine Nomine Associates
1
+ # Copyright (c) 2025-2026 Sine Nomine Associates
2
2
  #
3
3
  # Redistribution and use in source and binary forms, with or without
4
4
  # modification, are permitted provided that the following conditions are met:
@@ -25,7 +25,7 @@ from git_gerrit.git import Git
25
25
 
26
26
  DATABASE = "git-gerrit.db"
27
27
  MAGIC = 0x67697467 # "gitg"
28
- SCHEMA_VERSION = 1
28
+ SCHEMA_VERSION = 2
29
29
  MIGRATION_SCRIPTS = [
30
30
  """
31
31
  CREATE TABLE changes (
@@ -42,6 +42,39 @@ MIGRATION_SCRIPTS = [
42
42
  FOREIGN KEY (commit_id) REFERENCES commits(change_commit_id)
43
43
  );
44
44
  """,
45
+ """
46
+ ALTER TABLE changes RENAME TO gerrit_patchsets;
47
+ ALTER TABLE gerrit_patchsets RENAME COLUMN change_number TO number;
48
+ ALTER TABLE gerrit_patchsets RENAME COLUMN change_patchset TO patchset;
49
+ ALTER TABLE gerrit_patchsets RENAME COLUMN change_commit_id TO commit_id;
50
+
51
+ ALTER TABLE commits RENAME COLUMN commit_change_id TO change_id;
52
+ ALTER TABLE commits RENAME COLUMN commit_picked_from TO cherry_picked_from;
53
+ ALTER TABLE commits RENAME COLUMN commit_flags TO flags;
54
+
55
+ /* One canonical row per Change-Id. When several gerrit changes share a
56
+ Change-Id (a cherry-pick that reused it instead of generating a new
57
+ one), the lowest number is canonical and the rest are recorded in
58
+ gerrit_duplicate_changes. */
59
+ CREATE TABLE gerrit_changes (
60
+ number INTEGER PRIMARY KEY,
61
+ current_patchset INTEGER,
62
+ change_id TEXT NOT NULL UNIQUE,
63
+ merged_as TEXT /* NULL if not merged */
64
+ );
65
+ CREATE TABLE gerrit_duplicate_changes (
66
+ number INTEGER PRIMARY KEY, /* A non-canonical change number. */
67
+ change_id TEXT NOT NULL, /* Its Change-Id. */
68
+ canonical_number INTEGER NOT NULL, /* The canonical change number. */
69
+ FOREIGN KEY (canonical_number) REFERENCES gerrit_changes(number)
70
+ );
71
+ CREATE INDEX gerrit_duplicate_changes_change_id
72
+ ON gerrit_duplicate_changes (change_id);
73
+
74
+ /* Rescan every commit message so the canonical/duplicate split is built
75
+ from the whole history, not just changes seen since the upgrade. */
76
+ UPDATE commits SET flags = 0;
77
+ """,
45
78
  ]
46
79
 
47
80
 
@@ -163,9 +196,9 @@ class GitGerritDB:
163
196
  data = None
164
197
  return data
165
198
 
166
- def add_change(self, number, patchset, commit_id):
199
+ def add_patchset(self, number, patchset, commit_id):
167
200
  """
168
- Adds a new change to the database.
201
+ Adds a new patchset to the database.
169
202
 
170
203
  Args:
171
204
  number (int): The change number.
@@ -176,21 +209,128 @@ class GitGerritDB:
176
209
  cursor.execute(
177
210
  """
178
211
  INSERT OR IGNORE INTO commits
179
- (commit_id, commit_flags)
212
+ (commit_id, flags)
180
213
  VALUES (?, 0)
181
214
  """,
182
215
  (commit_id,),
183
216
  )
184
217
  cursor.execute(
185
218
  """
186
- INSERT OR IGNORE INTO changes
187
- (change_number, change_patchset, change_commit_id)
219
+ INSERT OR IGNORE INTO gerrit_patchsets
220
+ (number, patchset, commit_id)
188
221
  VALUES (?, ?, ?)
189
222
  """,
190
223
  (number, patchset, commit_id),
191
224
  )
192
225
  self._dirty = True
193
226
 
227
+ def record_change(self, number, current_patchset, change_id):
228
+ """
229
+ Records a change, keeping gerrit_changes to one row per Change-Id.
230
+
231
+ The canonical change for a Change-Id is the one with the lowest
232
+ number. Any other change numbers that carry the same Change-Id are
233
+ moved to gerrit_duplicate_changes.
234
+
235
+ Args:
236
+ number (int): The gerrit change number.
237
+ current_patchset (int): The most recent patchset number.
238
+ change_id (str): The gerrit Change-Id.
239
+
240
+ Returns:
241
+ bool: True if `number` is the canonical change, False if it was
242
+ recorded as a duplicate.
243
+ """
244
+ with Cursor(self) as cursor:
245
+ cursor.execute(
246
+ "SELECT number FROM gerrit_changes WHERE change_id = ?",
247
+ (change_id,),
248
+ )
249
+ row = cursor.fetchone()
250
+ canonical = row["number"] if row else None
251
+
252
+ if canonical == number:
253
+ # Rescan of the canonical change; just refresh the patchset.
254
+ cursor.execute(
255
+ "UPDATE gerrit_changes SET current_patchset = ? "
256
+ "WHERE number = ?",
257
+ (current_patchset, number),
258
+ )
259
+ self._dirty = True
260
+ return True
261
+
262
+ # Forget any stale record of this number before re-recording it.
263
+ cursor.execute("DELETE FROM gerrit_changes WHERE number = ?", (number,))
264
+ cursor.execute(
265
+ "DELETE FROM gerrit_duplicate_changes WHERE number = ?", (number,)
266
+ )
267
+ self._dirty = True
268
+
269
+ if canonical is not None and number > canonical:
270
+ # A lower-numbered change is canonical; sidebar this one.
271
+ cursor.execute(
272
+ "INSERT INTO gerrit_duplicate_changes "
273
+ "(number, change_id, canonical_number) VALUES (?, ?, ?)",
274
+ (number, change_id, canonical),
275
+ )
276
+ return False
277
+
278
+ # `number` becomes canonical: the Change-Id is new, or `number` is
279
+ # lower than the current canonical and takes it over. Demote the
280
+ # old canonical and re-point its duplicates.
281
+ demoted = []
282
+ if canonical is not None:
283
+ cursor.execute(
284
+ "SELECT number FROM gerrit_duplicate_changes "
285
+ "WHERE change_id = ?",
286
+ (change_id,),
287
+ )
288
+ demoted = [r["number"] for r in cursor.fetchall()]
289
+ demoted.append(canonical)
290
+ cursor.execute(
291
+ "DELETE FROM gerrit_duplicate_changes WHERE change_id = ?",
292
+ (change_id,),
293
+ )
294
+ cursor.execute(
295
+ "DELETE FROM gerrit_changes WHERE number = ?", (canonical,)
296
+ )
297
+ cursor.execute(
298
+ "INSERT INTO gerrit_changes "
299
+ "(number, current_patchset, change_id) VALUES (?, ?, ?)",
300
+ (number, current_patchset, change_id),
301
+ )
302
+ for dup in demoted:
303
+ cursor.execute(
304
+ "INSERT INTO gerrit_duplicate_changes "
305
+ "(number, change_id, canonical_number) VALUES (?, ?, ?)",
306
+ (dup, change_id, number),
307
+ )
308
+ return True
309
+
310
+ def get_change(self, change_id):
311
+ """
312
+ Returns the canonical change for a Change-Id, or None.
313
+
314
+ Args:
315
+ change_id (str): The gerrit Change-Id.
316
+
317
+ Returns:
318
+ dict: The gerrit_changes row, or None if the Change-Id is unknown.
319
+ """
320
+ with Cursor(self) as cursor:
321
+ cursor.execute(
322
+ "SELECT number, current_patchset, change_id, merged_as "
323
+ "FROM gerrit_changes WHERE change_id = ?",
324
+ (change_id,),
325
+ )
326
+ return self._as_dict(cursor.fetchone())
327
+
328
+ def count_duplicate_changes(self):
329
+ """Returns the number of rows in gerrit_duplicate_changes."""
330
+ with Cursor(self) as cursor:
331
+ cursor.execute("SELECT COUNT(*) FROM gerrit_duplicate_changes")
332
+ return cursor.fetchone()[0]
333
+
194
334
  def update_commit(self, commit_id, change_id, picked_from, flags):
195
335
  """
196
336
  Updates the details of a commit.
@@ -205,7 +345,7 @@ class GitGerritDB:
205
345
  cursor.execute(
206
346
  """
207
347
  UPDATE commits
208
- SET commit_change_id = ?, commit_picked_from = ?, commit_flags = ?
348
+ SET change_id = ?, cherry_picked_from = ?, flags = ?
209
349
  WHERE commit_id == ?
210
350
  """,
211
351
  (change_id, picked_from, flags, commit_id),
@@ -214,13 +354,16 @@ class GitGerritDB:
214
354
 
215
355
  def get_current_patchsets(self, limit=None):
216
356
  """
217
- Retrieves the current patchsets for all changes.
357
+ Retrieves the current patchset of every change.
358
+
359
+ Yields one row per change number: its highest-numbered patchset
360
+ joined with the commit details, ordered by change number descending.
218
361
 
219
362
  Args:
220
- limit (int, optional): The maximum number of patchsets to retrieve.
363
+ limit (int, optional): The maximum number of changes to retrieve.
221
364
 
222
365
  Yields:
223
- dict: A dictionary representing a patchset.
366
+ dict: A dictionary representing the current patchset of a change.
224
367
  """
225
368
  if self._dirty:
226
369
  self._conn.commit()
@@ -235,16 +378,16 @@ class GitGerritDB:
235
378
  cursor.execute(
236
379
  f"""
237
380
  SELECT
238
- ch.change_number AS number,
239
- MAX(ch.change_patchset) AS current_patchset,
240
- ch.change_commit_id AS commit_id,
241
- co.commit_change_id AS change_id,
242
- co.commit_picked_from AS cherry_picked_from,
243
- co.commit_flags AS flags
244
- FROM changes AS ch
245
- LEFT JOIN commits AS co ON co.commit_id = ch.change_commit_id
246
- GROUP BY ch.change_number
247
- ORDER BY ch.change_number DESC
381
+ ps.number AS number,
382
+ MAX(ps.patchset) AS current_patchset,
383
+ ps.commit_id AS commit_id,
384
+ co.change_id AS change_id,
385
+ co.cherry_picked_from AS cherry_picked_from,
386
+ co.flags AS flags
387
+ FROM gerrit_patchsets AS ps
388
+ LEFT JOIN commits AS co ON co.commit_id = ps.commit_id
389
+ GROUP BY ps.number
390
+ ORDER BY ps.number DESC
248
391
  {limit_clause}
249
392
  """
250
393
  )
@@ -269,17 +412,17 @@ class GitGerritDB:
269
412
  cursor.execute(
270
413
  """
271
414
  SELECT
272
- ch.change_number AS number,
273
- MAX(ch.change_patchset) AS current_patchset,
274
- ch.change_commit_id AS commit_id,
275
- co.commit_change_id AS change_id,
276
- co.commit_picked_from AS cherry_picked_from,
277
- co.commit_flags AS flags
278
- FROM changes AS ch
279
- LEFT JOIN commits AS co ON co.commit_id = ch.change_commit_id
280
- WHERE ch.change_number = ?
281
- GROUP BY ch.change_number
282
- ORDER BY ch.change_number DESC
415
+ ps.number AS number,
416
+ MAX(ps.patchset) AS current_patchset,
417
+ ps.commit_id AS commit_id,
418
+ co.change_id AS change_id,
419
+ co.cherry_picked_from AS cherry_picked_from,
420
+ co.flags AS flags
421
+ FROM gerrit_patchsets AS ps
422
+ LEFT JOIN commits AS co ON co.commit_id = ps.commit_id
423
+ WHERE ps.number = ?
424
+ GROUP BY ps.number
425
+ ORDER BY ps.number DESC
283
426
  LIMIT 1
284
427
  """,
285
428
  (number,),
@@ -307,15 +450,15 @@ class GitGerritDB:
307
450
  cursor.execute(
308
451
  """
309
452
  SELECT
310
- ch.change_number AS number,
311
- ch.change_patchset AS patchset,
312
- ch.change_commit_id AS commit_id,
313
- co.commit_change_id AS change_id,
314
- co.commit_picked_from AS cherry_picked_from,
315
- co.commit_flags AS flags
316
- FROM changes AS ch
317
- LEFT JOIN commits AS co ON co.commit_id = ch.change_commit_id
318
- WHERE ch.change_commit_id = ?
453
+ ps.number AS number,
454
+ ps.patchset AS patchset,
455
+ ps.commit_id AS commit_id,
456
+ co.change_id AS change_id,
457
+ co.cherry_picked_from AS cherry_picked_from,
458
+ co.flags AS flags
459
+ FROM gerrit_patchsets AS ps
460
+ LEFT JOIN commits AS co ON co.commit_id = ps.commit_id
461
+ WHERE ps.commit_id = ?
319
462
  LIMIT 1
320
463
  """,
321
464
  (commit_id,),
@@ -345,11 +488,11 @@ class GitGerritDB:
345
488
  """
346
489
  SELECT
347
490
  commit_id,
348
- commit_change_id,
349
- commit_picked_from,
350
- commit_flags
491
+ change_id,
492
+ cherry_picked_from,
493
+ flags
351
494
  FROM commits
352
- WHERE commit_picked_from = ?
495
+ WHERE cherry_picked_from = ?
353
496
  """,
354
497
  (commit_picked_from,),
355
498
  )
@@ -249,10 +249,13 @@ class Git:
249
249
  return False
250
250
 
251
251
  def change_id(self, sha1):
252
+ # Scan the raw commit message rather than git's %(trailers) parser.
253
+ # The trailer parser rejects the whole footer block when it contains
254
+ # non-trailer lines (e.g. the "FIXES 135591" convention used by
255
+ # OpenAFS), which would hide an otherwise valid Change-Id.
252
256
  change_id = None
253
- format_ = "%(trailers:key=Change-Id)"
254
- for line in self.log(sha1, max_count=1, pretty=format_):
255
- m = re.match(r'Change-Id: (I[0-9a-fA-F]+)', line)
257
+ for line in self.log(sha1, max_count=1, pretty="%B"):
258
+ m = re.match(r'^Change-Id: (I[0-9a-fA-F]+)', line)
256
259
  if m:
257
260
  change_id = m.group(1)
258
261
  return change_id
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: git_gerrit
3
- Version: 3.0.0
3
+ Version: 3.2.0
4
4
  Summary: Gerrit review system command line tools.
5
5
  Home-page: https://github.com/meffie/git-gerrit
6
6
  Author: Michael Meffie
@@ -39,7 +39,7 @@ with the **pygerrit2** package to access the Gerrit REST API.
39
39
  Commands
40
40
  ========
41
41
 
42
- .. code-block:: console
42
+ **git-gerrit** commands::
43
43
 
44
44
  git gerrit-checkout Fetch then checkout by gerrit number.
45
45
  git gerrit-cherry-pick Cherry pick from upstream branch by gerrit number to make a new gerrit.
@@ -57,16 +57,32 @@ Installation
57
57
  ============
58
58
 
59
59
  .. _pipx: https://pipx.pypa.io/stable/
60
+ .. _uv: https://github.com/astral-sh/uv
61
+
62
+ The recommended way to install **git-gerrit** is with `uv`_ or `pipx`_.
63
+
64
+ To install with ``uv``, run the following as a regular user:
60
65
 
61
- Install with `pipx`_::
66
+ .. code-block:: console
67
+
68
+ uv tool install git-gerrit
69
+ uv tool update-shell
62
70
 
63
- $ pipx install git-gerrit
71
+ The ``uv tool update-shell`` command adds the installed commands to your
72
+ shell's PATH. Restart your shell after running ``update-shell``.
64
73
 
65
- Be sure your ``PATH`` has been updated to run **pipx** installed commands::
74
+ To install with ``pipx_``, run the following as a regular user:
66
75
 
67
- $ pipx ensurepath
76
+ .. code-block:: console
68
77
 
69
- If **pipx** is not available, then install in a project local virtualenv::
78
+ pipx install git-gerrit
79
+ pipx ensurepath
80
+
81
+ The ``pipx ensurepath`` command adds the installed commands to your shell's
82
+ PATH. Restart you shell after running ``ensurepath``.
83
+
84
+ Alternatively, you can install **git-gerrit** directly into a local virtual
85
+ environment with ``pip``::
70
86
 
71
87
  $ cd <my-project-directory>
72
88
  $ python3 -m venv .venv
@@ -74,7 +90,6 @@ If **pipx** is not available, then install in a project local virtualenv::
74
90
  (.venv) $ pip install -U pip setuptools
75
91
  (.venv) $ pip install git-gerrit
76
92
 
77
-
78
93
  Setup
79
94
  =====
80
95
 
@@ -101,6 +116,16 @@ Setup a local OpenAFS git repo::
101
116
  $ git config --local gerrit.project openafs
102
117
  $ git gerrit-install-hooks
103
118
 
119
+ Fetch all of the change commits from the upstream gerrit system to your local
120
+ repository and create the local sqlite database. This is a heavy weight operation
121
+ and is only needed for **git gerrit-log** and **git gerrit-number**::
122
+
123
+ $ git gerrit-sync
124
+ ✔ Fetching changes from https://gerrit.example.com/example
125
+ ✔ Updating local database
126
+ ✔ Scanning commit messages
127
+ Done.
128
+
104
129
  Find open gerrits on the master branch::
105
130
 
106
131
  $ git gerrit-query is:open branch:master
@@ -163,14 +188,14 @@ Cherry-pick a gerrit onto the current branch::
163
188
 
164
189
  Show gerrit numbers in the checked out branch in the local git repo::
165
190
 
166
- $ git gerrit-log
191
+ $ git gerrit-log -n3
167
192
  12958 f47cb2d Suppress statement not reached warnings under Solaris Studio
168
193
  12957 306f0f3 afs: squash empty declaration warning
169
194
  12955 e006609 libafs: git ignore build artifacts on Solaris
170
195
 
171
196
  Show gerrit numbers by a revision in the local git repo::
172
197
 
173
- $ git gerrit-log openafs-stable-1_8_0
198
+ $ git gerrit-log -n3 openafs-stable-1_8_0
174
199
  12953 a08327f Update NEWS for 1.8.0 final release
175
200
  12938 acb0e84 afs_pioctl: avoid -Wpointer-sign
176
201
  12950 b73863b LINUX: fix RedHat 7.5 ENOTDIR issues
@@ -184,7 +209,7 @@ Show gerrit numbers by a range of revisions in the local git repo::
184
209
 
185
210
  Show just the gerrit numbers and subjects in the local git repo::
186
211
 
187
- $ git gerrit-log --format='{number}: {subject}'
212
+ $ git gerrit-log -n3 --format='{number}: {subject}'
188
213
  12958: Suppress statement not reached warnings under Solaris Studio
189
214
  12957: afs: squash empty declaration warning
190
215
  12955: libafs: git ignore build artifacts on Solaris
@@ -11,6 +11,7 @@ from git_gerrit.core import (
11
11
  sync,
12
12
  update,
13
13
  )
14
+ from git_gerrit.db import GitGerritDB, Cursor
14
15
  from git_gerrit.error import GitGerritError, GitGerritNotFoundError
15
16
 
16
17
 
@@ -105,10 +106,40 @@ def test_get_current_change__not_found(mock_modules):
105
106
 
106
107
  def test_sync(capsys, mock_modules):
107
108
  sync()
108
- output = capsys.readouterr().out.splitlines()
109
- print(output)
109
+ output = capsys.readouterr().out
110
110
  assert os.path.exists("mock-fetch")
111
111
  with open("mock-fetch", "r") as f:
112
112
  mock_fetch = f.read().splitlines()
113
113
  assert mock_fetch[0] == "https://gerrit.example.org/mayhem"
114
114
  assert mock_fetch[1] == "refs/changes/*:refs/changes/*"
115
+
116
+ # The mocked show-ref reports three changes:
117
+ # change 1 - current patchset (ps 3) has Change-Id Ibc6dab9f...
118
+ # change 2 - current patchset has no Change-Id trailer
119
+ # change 3 - current patchset reuses change 1's Change-Id
120
+ with GitGerritDB() as db:
121
+ with Cursor(db) as cursor:
122
+ cursor.execute(
123
+ "SELECT number, current_patchset, change_id FROM gerrit_changes "
124
+ "ORDER BY number"
125
+ )
126
+ changes = [tuple(row) for row in cursor.fetchall()]
127
+ with Cursor(db) as cursor:
128
+ cursor.execute(
129
+ "SELECT number, canonical_number FROM gerrit_duplicate_changes"
130
+ )
131
+ duplicates = [tuple(row) for row in cursor.fetchall()]
132
+ with Cursor(db) as cursor:
133
+ cursor.execute(
134
+ "SELECT flags FROM commits WHERE commit_id = ?", (f"{4:040}",)
135
+ )
136
+ skipped_commit = cursor.fetchone()
137
+
138
+ cid = "Ibc6dab9f4a99d693ea03891ed7222fed9a07a85a"
139
+ # Change 1 is canonical (lowest number); change 3 is sidebarred as a
140
+ # duplicate; change 2 is skipped for want of a Change-Id.
141
+ assert changes == [(1, 3, cid)]
142
+ assert duplicates == [(3, 1)]
143
+ assert "Skipped 1 change(s) with no Change-Id." in output
144
+ # The skipped change's commit is still recorded and marked scanned.
145
+ assert skipped_commit["flags"] == 1
@@ -0,0 +1,205 @@
1
+ import pytest
2
+
3
+ from git_gerrit.db import GitGerritDB, Cursor, SCHEMA_VERSION
4
+
5
+
6
+ @pytest.fixture
7
+ def db(mock_modules):
8
+ with GitGerritDB() as db:
9
+ yield db
10
+
11
+
12
+ @pytest.fixture
13
+ def staged_db(mock_modules):
14
+ with GitGerritDB() as db:
15
+ db.add_patchset(101, 1, "aaa")
16
+ db.add_patchset(101, 2, "bbb")
17
+ db.add_patchset(102, 1, "ccc")
18
+ db.add_patchset(103, 1, "ddd")
19
+ db.add_patchset(103, 2, "eee")
20
+ db.add_patchset(103, 3, "fff")
21
+ db.update_commit("bbb", "I101", None, 0)
22
+ db.update_commit("ccc", "I102", None, 1)
23
+ db.update_commit("fff", "I103", "ggg", 0)
24
+ yield db
25
+
26
+
27
+ def test_db_get_schema_version__returns_current_schema_version(db):
28
+ assert db._get_schema_version() == SCHEMA_VERSION
29
+
30
+
31
+ def test_db_init__creates_tables(db):
32
+ with Cursor(db) as cursor:
33
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
34
+ names = [row['name'] for row in cursor.fetchall()]
35
+ assert "commits" in names
36
+ assert "gerrit_patchsets" in names
37
+ assert "gerrit_changes" in names
38
+ assert "gerrit_duplicate_changes" in names
39
+
40
+
41
+ def test_db_add_patchset__inserts_into_tables(db):
42
+ db.add_patchset(123, 1, "abc")
43
+ with Cursor(db) as cursor:
44
+ cursor.execute("SELECT * FROM gerrit_patchsets WHERE number=123")
45
+ change = cursor.fetchone()
46
+ assert change is not None
47
+ assert change["patchset"] == 1
48
+ assert change["commit_id"] == "abc"
49
+
50
+ cursor.execute("SELECT * FROM commits WHERE commit_id='abc'")
51
+ commit = cursor.fetchone()
52
+ assert commit is not None
53
+ assert commit["flags"] == 0
54
+
55
+
56
+ def _dup_rows(db):
57
+ with Cursor(db) as cursor:
58
+ cursor.execute(
59
+ "SELECT number, change_id, canonical_number "
60
+ "FROM gerrit_duplicate_changes ORDER BY number"
61
+ )
62
+ return [tuple(row) for row in cursor.fetchall()]
63
+
64
+
65
+ def _change_rows(db):
66
+ with Cursor(db) as cursor:
67
+ cursor.execute(
68
+ "SELECT number, current_patchset, change_id "
69
+ "FROM gerrit_changes ORDER BY number"
70
+ )
71
+ return [tuple(row) for row in cursor.fetchall()]
72
+
73
+
74
+ def test_db_record_change__inserts_and_updates_canonical(db):
75
+ assert db.record_change(200, 1, "I200") is True
76
+ assert _change_rows(db) == [(200, 1, "I200")]
77
+
78
+ # A rescan of the same change refreshes the patchset in place.
79
+ assert db.record_change(200, 2, "I200") is True
80
+ assert _change_rows(db) == [(200, 2, "I200")]
81
+ assert _dup_rows(db) == []
82
+
83
+
84
+ def test_db_record_change__sidebars_higher_numbered_duplicate(db):
85
+ db.record_change(300, 1, "Ishared")
86
+ assert db.record_change(305, 1, "Ishared") is False
87
+
88
+ assert _change_rows(db) == [(300, 1, "Ishared")]
89
+ assert _dup_rows(db) == [(305, "Ishared", 300)]
90
+
91
+
92
+ def test_db_record_change__lower_number_takes_over_as_canonical(db):
93
+ # Changes arrive newest-first, as git-gerrit-sync scans them.
94
+ db.record_change(305, 1, "Ishared")
95
+ db.record_change(303, 1, "Ishared")
96
+ db.record_change(300, 1, "Ishared")
97
+
98
+ assert _change_rows(db) == [(300, 1, "Ishared")]
99
+ assert _dup_rows(db) == [(303, "Ishared", 300), (305, "Ishared", 300)]
100
+
101
+
102
+ def test_db_record_change__canonical_stable_when_scanned_oldest_first(db):
103
+ db.record_change(300, 1, "Ishared")
104
+ db.record_change(303, 1, "Ishared")
105
+ db.record_change(305, 1, "Ishared")
106
+
107
+ assert _change_rows(db) == [(300, 1, "Ishared")]
108
+ assert _dup_rows(db) == [(303, "Ishared", 300), (305, "Ishared", 300)]
109
+
110
+
111
+ def test_db_record_change__is_idempotent_across_rescans(db):
112
+ for _ in range(2):
113
+ db.record_change(305, 1, "Ishared")
114
+ db.record_change(303, 2, "Ishared")
115
+ db.record_change(300, 3, "Ishared")
116
+
117
+ assert _change_rows(db) == [(300, 3, "Ishared")]
118
+ assert _dup_rows(db) == [(303, "Ishared", 300), (305, "Ishared", 300)]
119
+
120
+
121
+ def test_db_get_change__returns_canonical_only(db):
122
+ db.record_change(300, 1, "Ishared")
123
+ db.record_change(305, 2, "Ishared")
124
+
125
+ assert db.get_change("Ishared")["number"] == 300
126
+ assert db.get_change("Imissing") is None
127
+
128
+
129
+ def test_db_count_duplicate_changes(db):
130
+ db.record_change(300, 1, "Ishared")
131
+ db.record_change(305, 1, "Ishared")
132
+ db.record_change(400, 1, "Iunique")
133
+
134
+ assert db.count_duplicate_changes() == 1
135
+
136
+
137
+ def test_db_update_commit__updates_commit_row(db):
138
+ db.add_patchset(123, 1, "abc")
139
+ db.update_commit("abc", "I123", "def", 1)
140
+ with Cursor(db) as cursor:
141
+ cursor.execute("SELECT * FROM commits WHERE commit_id='abc'")
142
+ commit = cursor.fetchone()
143
+ assert commit is not None
144
+ assert commit["change_id"] == "I123"
145
+ assert commit["cherry_picked_from"] == "def"
146
+ assert commit["flags"] == 1
147
+
148
+
149
+ def test_db_get_current_patchsets__returns_latest_patchsets(staged_db):
150
+ db = staged_db
151
+
152
+ # Test without filters
153
+ changes = list(db.get_current_patchsets())
154
+ assert len(changes) == 3
155
+ assert changes[0]["number"] == 103
156
+ assert changes[0]["current_patchset"] == 3
157
+ assert changes[0]["commit_id"] == "fff"
158
+ assert changes[0]["change_id"] == "I103"
159
+ assert changes[0]["cherry_picked_from"] == "ggg"
160
+ assert changes[0]["flags"] == 0
161
+
162
+ assert changes[1]["number"] == 102
163
+ assert changes[1]["current_patchset"] == 1
164
+ assert changes[1]["commit_id"] == "ccc"
165
+ assert changes[1]["change_id"] == "I102"
166
+ assert changes[1]["flags"] == 1
167
+
168
+ assert changes[2]["number"] == 101
169
+ assert changes[2]["current_patchset"] == 2
170
+ assert changes[2]["commit_id"] == "bbb"
171
+ assert changes[2]["change_id"] == "I101"
172
+ assert changes[2]["flags"] == 0
173
+
174
+ # Test with limit
175
+ changes = list(db.get_current_patchsets(limit=1))
176
+ assert len(changes) == 1
177
+ assert changes[0]["number"] == 103
178
+
179
+
180
+ def test_db_get_current_patchset__returns_latest_patchset_by_number(staged_db):
181
+ db = staged_db
182
+ change = db.get_current_patchset_by_number(101)
183
+ assert change["number"] == 101
184
+ assert change["current_patchset"] == 2
185
+ assert change["commit_id"] == "bbb"
186
+ assert change["change_id"] == "I101"
187
+ assert change["flags"] == 0
188
+
189
+ change = db.get_current_patchset_by_number(102)
190
+ assert change["number"] == 102
191
+ assert change["current_patchset"] == 1
192
+ assert change["commit_id"] == "ccc"
193
+ assert change["change_id"] == "I102"
194
+ assert change["flags"] == 1
195
+
196
+ change = db.get_current_patchset_by_number(103)
197
+ assert change["number"] == 103
198
+ assert change["current_patchset"] == 3
199
+ assert change["commit_id"] == "fff"
200
+ assert change["change_id"] == "I103"
201
+ assert change["cherry_picked_from"] == "ggg"
202
+ assert change["flags"] == 0
203
+
204
+ change = db.get_current_patchset_by_number(999)
205
+ assert change is None
@@ -84,5 +84,7 @@ def test_write_hook(git):
84
84
 
85
85
 
86
86
  def test_change_id(git):
87
+ # The Change-Id is found even though the footer also contains non-trailer
88
+ # "FIXES" lines that defeat git's %(trailers) parser.
87
89
  got = git.change_id("0" * 40)
88
- assert got == "I68fd140aab7e65bec1ac537d19de89f9d32443c1"
90
+ assert got == "Ibc6dab9f4a99d693ea03891ed7222fed9a07a85a"
@@ -1 +0,0 @@
1
- VERSION = "3.0.0"
@@ -1,121 +0,0 @@
1
- import pytest
2
-
3
- from git_gerrit.db import GitGerritDB, Cursor, SCHEMA_VERSION
4
-
5
-
6
- @pytest.fixture
7
- def db(mock_modules):
8
- with GitGerritDB() as db:
9
- yield db
10
-
11
-
12
- @pytest.fixture
13
- def staged_db(mock_modules):
14
- with GitGerritDB() as db:
15
- db.add_change(101, 1, "aaa")
16
- db.add_change(101, 2, "bbb")
17
- db.add_change(102, 1, "ccc")
18
- db.add_change(103, 1, "ddd")
19
- db.add_change(103, 2, "eee")
20
- db.add_change(103, 3, "fff")
21
- db.update_commit("bbb", "I101", None, 0)
22
- db.update_commit("ccc", "I102", None, 1)
23
- db.update_commit("fff", "I103", "ggg", 0)
24
- yield db
25
-
26
-
27
- def test_db_get_schema_version__returns_current_schema_version(db):
28
- assert db._get_schema_version() == SCHEMA_VERSION
29
-
30
-
31
- def test_db_init__creates_tables(db):
32
- with Cursor(db) as cursor:
33
- cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
34
- names = [row['name'] for row in cursor.fetchall()]
35
- assert len(names) != 0
36
-
37
-
38
- def test_db_add_change__inserts_into_tables(db):
39
- db.add_change(123, 1, "abc")
40
- with Cursor(db) as cursor:
41
- cursor.execute("SELECT * FROM changes WHERE change_number=123")
42
- change = cursor.fetchone()
43
- assert change is not None
44
- assert change["change_patchset"] == 1
45
- assert change["change_commit_id"] == "abc"
46
-
47
- cursor.execute("SELECT * FROM commits WHERE commit_id='abc'")
48
- commit = cursor.fetchone()
49
- assert commit is not None
50
- assert commit["commit_flags"] == 0
51
-
52
-
53
- def test_db_update_commit__updates_commit_row(db):
54
- db.add_change(123, 1, "abc")
55
- db.update_commit("abc", "I123", "def", 1)
56
- with Cursor(db) as cursor:
57
- cursor.execute("SELECT * FROM commits WHERE commit_id='abc'")
58
- commit = cursor.fetchone()
59
- assert commit is not None
60
- assert commit["commit_change_id"] == "I123"
61
- assert commit["commit_picked_from"] == "def"
62
- assert commit["commit_flags"] == 1
63
-
64
-
65
- def test_db_get_current_patchsets__returns_latest_patchsets(staged_db):
66
- db = staged_db
67
-
68
- # Test without filters
69
- changes = list(db.get_current_patchsets())
70
- assert len(changes) == 3
71
- assert changes[0]["number"] == 103
72
- assert changes[0]["current_patchset"] == 3
73
- assert changes[0]["commit_id"] == "fff"
74
- assert changes[0]["change_id"] == "I103"
75
- assert changes[0]["cherry_picked_from"] == "ggg"
76
- assert changes[0]["flags"] == 0
77
-
78
- assert changes[1]["number"] == 102
79
- assert changes[1]["current_patchset"] == 1
80
- assert changes[1]["commit_id"] == "ccc"
81
- assert changes[1]["change_id"] == "I102"
82
- assert changes[1]["flags"] == 1
83
-
84
- assert changes[2]["number"] == 101
85
- assert changes[2]["current_patchset"] == 2
86
- assert changes[2]["commit_id"] == "bbb"
87
- assert changes[2]["change_id"] == "I101"
88
- assert changes[2]["flags"] == 0
89
-
90
- # Test with limit
91
- changes = list(db.get_current_patchsets(limit=1))
92
- assert len(changes) == 1
93
- assert changes[0]["number"] == 103
94
-
95
-
96
- def test_db_get_current_patchset__returns_latest_patchset_by_number(staged_db):
97
- db = staged_db
98
- change = db.get_current_patchset_by_number(101)
99
- assert change["number"] == 101
100
- assert change["current_patchset"] == 2
101
- assert change["commit_id"] == "bbb"
102
- assert change["change_id"] == "I101"
103
- assert change["flags"] == 0
104
-
105
- change = db.get_current_patchset_by_number(102)
106
- assert change["number"] == 102
107
- assert change["current_patchset"] == 1
108
- assert change["commit_id"] == "ccc"
109
- assert change["change_id"] == "I102"
110
- assert change["flags"] == 1
111
-
112
- change = db.get_current_patchset_by_number(103)
113
- assert change["number"] == 103
114
- assert change["current_patchset"] == 3
115
- assert change["commit_id"] == "fff"
116
- assert change["change_id"] == "I103"
117
- assert change["cherry_picked_from"] == "ggg"
118
- assert change["flags"] == 0
119
-
120
- change = db.get_current_patchset_by_number(999)
121
- assert change is None
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes