[projects/git-slug] Reorganizacja struktury projektu

arekm arekm at pld-linux.org
Tue May 12 15:08:35 CEST 2026


commit 6c874ae8238e45ed3624c9558e74e1611df6f999
Author: Arkadiusz Miśkiewicz <arekm at maven.pl>
Date:   Mon Apr 6 23:10:21 2026 +0200

    Reorganizacja struktury projektu
    
    Komponenty serwerowe przeniesione do server/:
    - server/hooks/ — gitolite post-receive + slug_hook.py
    - server/adc/ — komendy administracyjne gitolite (move, trash)
    - server/slug_watch/ — daemon, konfiguracja, init, service, cron
    - server/README.md — opis komponentów serwerowych
    
    Daemon/daemon.py (~40 linii) wklejony bezpośrednio do slug_watch —
    eliminuje osobny pakiet Python z site-packages, slug_watch jest
    teraz samodzielny.
    
    Dokumentacja:
    - doc/man/slug.txt — nowa strona man dla slug (aktualne opcje i komendy)
    - doc/man/slug_watch.txt — nowa strona man dla slug_watch
    - README.md zaktualizowany (make test, benchmarki, struktura projektu,
      domyślne jobs, nowe testy)
    
    Build:
    - Makefile: zmienne PYTHON, ASCIIDOC, XMLTO, INSTALL do nadpisywania,
      target test (python3 -m pytest), man buduje do doc/man/
    - setup.py: usunięty pakiet Daemon, ścieżki zaktualizowane na server/
    - MANIFEST.in: zaktualizowany na nowe ścieżki
    
    Porządki:
    - Usunięty sys.path.insert hack z slug.py (był tylko do testów lokalnych)
    - .gitignore: dodany *.swp, uproszczone wzorce __pycache__
    - tests/test_benchmark.py — benchmarki pytest-benchmark (check_remote,
      find_git_repos, git_config_get)
    
    Wymaga aktualizacji git-core-slug.spec (osobna sesja).

 .gitignore                                         |   3 +-
 Daemon/__init__.py                                 |   1 -
 Daemon/daemon.py                                   | 122 ---------------------
 MANIFEST.in                                        |   9 --
 Makefile                                           |  33 ++++--
 README.md                                          |  28 ++++-
 man/git-pld.1.txt => doc/man/slug.txt              |  10 +-
 doc/man/slug_watch.txt                             | 109 ++++++++++++++++++
 server/README.md                                   |  37 +++++++
 {adc => server/adc}/move                           |   0
 {adc => server/adc}/trash                          |   0
 post-receive => server/hooks/post-receive          |   0
 .../hooks}/slug_hook.py                            |   0
 {watch => server/slug_watch}/crontab               |   0
 {watch => server/slug_watch}/slug_watch            |  96 +++++++++++++++-
 {watch => server/slug_watch}/slug_watch-cron       |   0
 {watch => server/slug_watch}/slug_watch.init       |   0
 {watch => server/slug_watch}/slug_watch.service    |   0
 {watch => server/slug_watch}/slug_watch.sysconfig  |   0
 setup.py                                           |   6 +-
 slug.py                                            |   3 -
 tests/test_benchmark.py                            |  90 +++++++++++++++
 22 files changed, 384 insertions(+), 163 deletions(-)
---
diff --git a/.gitignore b/.gitignore
index 9b2a450..730a9d2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,5 +2,4 @@ MANIFEST
 .coverage
 .pytest_cache/
 __pycache__/
-git_slug/__pycache__/
-tests/__pycache__/
+*.swp
diff --git a/Daemon/__init__.py b/Daemon/__init__.py
deleted file mode 100644
index d39a83b..0000000
--- a/Daemon/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-#empty file
diff --git a/Daemon/daemon.py b/Daemon/daemon.py
deleted file mode 100644
index 164f793..0000000
--- a/Daemon/daemon.py
+++ /dev/null
@@ -1,122 +0,0 @@
-"""Generic linux daemon base class for python 3.x."""
-
-import sys, os, time, atexit, signal
-
-class daemon:
-        """A generic daemon class.
-
-        Usage: subclass the daemon class and override the run() method."""
-
-        def __init__(self, pidfile): self.pidfile = pidfile
-
-        def daemonize(self):
-                """Deamonize class. UNIX double fork mechanism."""
-
-                try:
-                        pid = os.fork()
-                        if pid > 0:
-                                # exit first parent
-                                sys.exit(0)
-                except OSError as err:
-                        sys.stderr.write('fork #1 failed: {0}\n'.format(err))
-                        sys.exit(1)
-
-                # decouple from parent environment
-                os.chdir('/')
-                os.setsid()
-                os.umask(0o033)
-
-                # do second fork
-                try:
-                        pid = os.fork()
-                        if pid > 0:
-
-                                # exit from second parent
-                                sys.exit(0)
-                except OSError as err:
-                        sys.stderr.write('fork #2 failed: {0}\n'.format(err))
-                        sys.exit(1)
-
-                # redirect standard file descriptors
-                sys.stdout.flush()
-                sys.stderr.flush()
-                si = open(os.devnull, 'r')
-                so = open(os.devnull, 'a+')
-                se = open(os.devnull, 'a+')
-
-                os.dup2(si.fileno(), sys.stdin.fileno())
-                os.dup2(so.fileno(), sys.stdout.fileno())
-                os.dup2(se.fileno(), sys.stderr.fileno())
-
-                # write pidfile
-                atexit.register(self.delpid)
-
-                pid = str(os.getpid())
-                with open(self.pidfile,'w+') as f:
-                        f.write(pid + '\n')
-
-        def delpid(self):
-                os.remove(self.pidfile)
-
-        def start(self):
-                """Start the daemon."""
-
-                # Check for a pidfile to see if the daemon already runs
-                try:
-                        with open(self.pidfile,'r') as pf:
-
-                                pid = int(pf.read().strip())
-                except IOError:
-                        pid = None
-
-                if pid:
-                        message = "pidfile {0} already exist. " + \
-                                        "Daemon already running?\n"
-                        sys.stderr.write(message.format(self.pidfile))
-                        sys.exit(1)
-
-                # Start the daemon
-                self.daemonize()
-                self.run()
-
-        def stop(self):
-                """Stop the daemon."""
-
-                # Get the pid from the pidfile
-                try:
-                        with open(self.pidfile,'r') as pf:
-                                pid = int(pf.read().strip())
-                except IOError:
-                        pid = None
-
-                if not pid:
-                        message = "pidfile {0} does not exist. " + \
-                                        "Daemon not running?\n"
-                        sys.stderr.write(message.format(self.pidfile))
-                        return # not an error in a restart
-
-                # Try killing the daemon process
-                try:
-                        while 1:
-                                os.kill(pid, signal.SIGTERM)
-                                time.sleep(0.1)
-                except OSError as err:
-                        e = str(err.args)
-                        if e.find("No such process") > 0:
-                                if os.path.exists(self.pidfile):
-                                        os.remove(self.pidfile)
-                        else:
-                                print (str(err.args))
-                                sys.exit(1)
-
-        def restart(self):
-                """Restart the daemon."""
-                self.stop()
-                self.start()
-
-        def run(self):
-                """You should override this method when you subclass Daemon.
-
-                It will be called after the process has been daemonized by
-                start() or restart()."""
-
diff --git a/MANIFEST.in b/MANIFEST.in
deleted file mode 100644
index 2886566..0000000
--- a/MANIFEST.in
+++ /dev/null
@@ -1,9 +0,0 @@
-include README.md
-include Makefile
-include slug.py.txt
-recursive-include post-receive.python.d *.py
-include watch/crontab
-include watch/slug_watch-cron
-include watch/slug_watch.init
-include watch/slug_watch.service
-include watch/slug_watch.sysconfig
diff --git a/Makefile b/Makefile
index 885025a..ee99658 100644
--- a/Makefile
+++ b/Makefile
@@ -1,17 +1,30 @@
-MANDIR=/usr/share/man
+PYTHON = python3
+ASCIIDOC = asciidoc
+XMLTO = xmlto
+INSTALL = install
+MANDIR = /usr/share/man
+MANPAGES = doc/man/slug.1 doc/man/slug_watch.1
 
-man: man/git-pld.1
+man: $(MANPAGES)
 
-man-install: man/git-pld.1
-	install -D man/git-pld.1 $(DESTDIR)$(MANDIR)/man1/git-pld.1
+man-install: $(MANPAGES)
+	$(INSTALL) -D doc/man/slug.1 $(DESTDIR)$(MANDIR)/man1/slug.1
+	$(INSTALL) -D doc/man/slug_watch.1 $(DESTDIR)$(MANDIR)/man1/slug_watch.1
 
-man/git-pld.1: man/git-pld.1.xml
-	xmlto man -o man $<
+doc/man/slug.1: doc/man/slug.txt
+	$(ASCIIDOC) -b docbook -d manpage -o doc/man/slug.xml $<
+	$(XMLTO) man -o doc/man doc/man/slug.xml
+	rm -f doc/man/slug.xml
 
-man/git-pld.1.xml: man/git-pld.1.txt
-	asciidoc -b docbook -d manpage -o $@ $<
+doc/man/slug_watch.1: doc/man/slug_watch.txt
+	$(ASCIIDOC) -b docbook -d manpage -o doc/man/slug_watch.xml $<
+	$(XMLTO) man -o doc/man doc/man/slug_watch.xml
+	rm -f doc/man/slug_watch.xml
+
+test:
+	$(PYTHON) -m pytest
 
 clean:
-	rm -f man/git-pld.1.xml man/git-pld.1
+	rm -f doc/man/*.xml $(MANPAGES)
 
-.PHONY: man man-install clean
+.PHONY: man man-install clean test
diff --git a/README.md b/README.md
index 6079882..e5b0634 100644
--- a/README.md
+++ b/README.md
@@ -33,20 +33,26 @@ python3 -m pip install pytest pytest-cov
 Run the test suite:
 
 ```sh
-pytest
+make test
 ```
 
-Run the suite with coverage:
+Run with coverage:
 
 ```sh
-pytest --cov=slug --cov=git_slug --cov-report=term-missing
+python3 -m pytest --cov=slug --cov=git_slug --cov-report=term-missing
+```
+
+Run benchmarks only:
+
+```sh
+python3 -m pytest --benchmark-only
 ```
 
 Run a focused subset while working on one area:
 
 ```sh
-pytest tests/test_cli.py
-pytest tests/test_slug_commands.py
+python3 -m pytest tests/test_cli.py
+python3 -m pytest tests/test_slug_commands.py
 ```
 
 The test suite is intentionally lightweight:
@@ -64,6 +70,8 @@ Current test coverage is split roughly by responsibility:
 - `tests/test_slug_commands.py` checks `update`, `clone`, `list`, and `init` command wiring.
 - `tests/test_failure_paths.py` checks common error paths that previously regressed.
 - `tests/test_refsdata.py` and `tests/test_gitrepo.py` cover small helper-module behavior.
+- `tests/test_proctrack.py` tests subprocess tracking for clean Ctrl-C shutdown.
+- `tests/test_benchmark.py` performance benchmarks (pytest-benchmark).
 
 ## Setup
 
@@ -152,7 +160,7 @@ git pld [-d DIR] [-j N] [-q] [--pattern PAT] <command> [<git-args>...]
 | Option | Description |
 |--------|-------------|
 | `-d`, `--packagesdir DIR` | Local directory with repos (default: `~/rpm/packages`) |
-| `-j`, `--jobs N` | Number of parallel workers (default: CPU count) |
+| `-j`, `--jobs N` | Number of parallel workers (default: `min(cpu_count*4, 32)`) |
 | `-q`, `--quiet` | Suppress stdout from successful repos |
 | `--pattern PAT` | Repo name glob, repeatable (default: `*`) |
 | `--version` | Print version |
@@ -201,6 +209,14 @@ git pld pull --help         # git pull man page
 git pld update --help       # slug update options
 ```
 
+## Project structure
+
+- `slug.py` — client CLI entry point
+- `git_slug/` — client library (gitrepo, refsdata, proctrack, constants)
+- `server/` — server-side components (gitolite hooks, slug_watch daemon, admin commands); see `server/README.md`
+- `doc/man/` — man page sources (asciidoc); build with `make man`
+- `tests/` — test suite
+
 ## License
 
 See [COPYING](COPYING).
diff --git a/man/git-pld.1.txt b/doc/man/slug.txt
similarity index 95%
rename from man/git-pld.1.txt
rename to doc/man/slug.txt
index a3e5a3b..919c712 100644
--- a/man/git-pld.1.txt
+++ b/doc/man/slug.txt
@@ -1,9 +1,9 @@
-git-pld(1)
-==========
+slug(1)
+=======
 
 NAME
 ----
-git-pld - run git commands across PLD Linux package repositories
+slug - run git commands across PLD Linux package repositories
 
 
 SYNOPSIS
@@ -16,7 +16,7 @@ SYNOPSIS
 DESCRIPTION
 -----------
 
-git-pld runs git commands across PLD Linux package repositories in
+slug runs git commands across PLD Linux package repositories in
 parallel.  It has two kinds of commands:
 
 Slug-specific commands ('update', 'clone', 'list', 'init') provide
@@ -164,7 +164,7 @@ in gitconfig (see CONFIGURATION below).
 CONFIGURATION
 -------------
 
-git-pld reads settings from the '[PLD]' section of '~/.gitconfig'
+slug reads settings from the '[PLD]' section of '~/.gitconfig'
 (or 'XDG_CONFIG_HOME/git/config').  CLI options take priority over
 gitconfig values, which take priority over built-in defaults.
 
diff --git a/doc/man/slug_watch.txt b/doc/man/slug_watch.txt
new file mode 100644
index 0000000..5b5bf18
--- /dev/null
+++ b/doc/man/slug_watch.txt
@@ -0,0 +1,109 @@
+slug_watch(1)
+=============
+
+NAME
+----
+slug_watch - daemon to update PLD package refs on git push
+
+
+SYNOPSIS
+--------
+[verse]
+'slug_watch' -w <watchdir> -r <refrepodir> [<options>]
+'slug_watch' -w <watchdir> -r <refrepodir> -d [start|stop]
+
+
+DESCRIPTION
+-----------
+
+slug_watch is a server-side daemon that maintains the centralized Refs
+repository used by linkgit:git-pld[1].  It watches a directory for
+notification files written by gitolite post-receive hooks, and updates
+the Refs repo and projects list accordingly.
+
+When a developer pushes to a package repository, gitolite writes a
+notification file to the watch directory.  slug_watch picks it up via
+inotify, merges the updated refs into the Refs repo ('heads' file),
+updates 'projects.list' for gitweb, and commits the change.
+
+On startup, slug_watch processes any notification files already present
+in the watch directory (sorted by mtime), then enters the inotify loop.
+
+
+OPTIONS
+-------
+
+-w <directory>::
+--watchdir <directory>::
+    Directory to watch for notification files from gitolite hooks.
+    Required.  If the path is not absolute, it is interpreted relative
+    to the home directory of the user running the daemon.
+
+-r <directory>::
+--refrepodir <directory>::
+    Directory containing the bare Refs git repository ('Refs.git').
+    Required.  If the path is not absolute, it is interpreted relative
+    to the home directory of the user running the daemon.
+
+-d [start|stop]::
+--daemon [start|stop]::
+    Run as a daemon.  'start' forks into background and writes a PID
+    file to '/var/run/slug_watch.pid'.  'stop' reads the PID file and
+    sends SIGTERM.  If omitted, runs in foreground.
+
+-u <user>::
+--user <user>::
+    Drop privileges to the specified user after starting.  Sets UID,
+    GID, and HOME.
+
+-m <address>::
+--maillogs <address>::
+    Send log messages via email to the specified address.  Repeatable
+    for multiple recipients.  Requires '-s'.
+
+-s <address>::
+--sender <address>::
+    Sender address for log emails.  Required when '-m' is used.
+
+
+FILES
+-----
+
+/etc/sysconfig/slug_watch::
+    Environment file for the systemd service.  Variables:
++
+--
+WATCHDIR;;
+    Watch directory path.
+REFREPODIR;;
+    Refs repository directory path.
+OTHER_OPTIONS;;
+    Additional command-line options (e.g. '-m root -s git').
+--
+
+slug_watch.service::
+    Systemd unit file.  Reads '/etc/sysconfig/slug_watch' and runs
+    slug_watch as the 'git' user.
+
+slug_watch-cron::
+    Cron helper script that runs 'git gc' on the Refs repository.
+    Typically scheduled every 15 minutes via crontab.
+
+slug_watch.lock::
+    Lock file (in the working directory) to prevent multiple instances.
+
+
+EXIT STATUS
+-----------
+
+0::
+    Clean shutdown (SIGTERM received).
+
+1::
+    Error (lock file held by another instance, or fatal exception).
+
+
+SEE ALSO
+--------
+
+linkgit:git-pld[1]
diff --git a/server/README.md b/server/README.md
new file mode 100644
index 0000000..733edda
--- /dev/null
+++ b/server/README.md
@@ -0,0 +1,37 @@
+# Server-side components
+
+These files run on the PLD Linux gitolite server, not on developer machines.
+
+## hooks/
+
+Gitolite hooks triggered on every `git push` to a package repository.
+
+- **post-receive** — generic hook dispatcher. Runs scripts from
+  `hooks/post-receive.d/` and Python plugins from
+  `hooks/post-receive.python.d/`.
+- **slug_hook.py** — plugin loaded by post-receive. Writes a notification
+  file (pusher, repo name, changed refs) to the watch directory for
+  slug_watch to pick up.
+
+## slug_watch/
+
+Daemon that maintains the centralized Refs repository. The Refs repo is
+what makes `git pld update` fast — clients fetch one small file instead
+of querying 20k+ repos individually.
+
+- **slug_watch** — the daemon itself. Watches for notification files via
+  inotify, merges updated refs into the Refs repo, updates
+  `projects.list` for gitweb, and commits the change.
+- **slug_watch.service** — systemd unit.
+- **slug_watch.init** — SysV init script (legacy).
+- **slug_watch.sysconfig** — environment config (`WATCHDIR`, `REFREPODIR`).
+- **slug_watch-cron** — runs `git gc` on the Refs repo periodically.
+- **crontab** — cron schedule for the above (every 15 minutes).
+
+## adc/
+
+Gitolite ADC (Admin Defined Commands) for repository management.
+
+- **move** — move/copy a package repository (also mirrors to GitHub and
+  notifies the mailing list). The `copy` command is a symlink to `move`.
+- **trash** — archive a deleted repository to the ATTIC directory.
diff --git a/adc/move b/server/adc/move
similarity index 100%
rename from adc/move
rename to server/adc/move
diff --git a/adc/trash b/server/adc/trash
similarity index 100%
rename from adc/trash
rename to server/adc/trash
diff --git a/post-receive b/server/hooks/post-receive
similarity index 100%
rename from post-receive
rename to server/hooks/post-receive
diff --git a/post-receive.python.d/slug_hook.py b/server/hooks/slug_hook.py
similarity index 100%
rename from post-receive.python.d/slug_hook.py
rename to server/hooks/slug_hook.py
diff --git a/watch/crontab b/server/slug_watch/crontab
similarity index 100%
rename from watch/crontab
rename to server/slug_watch/crontab
diff --git a/watch/slug_watch b/server/slug_watch/slug_watch
similarity index 70%
rename from watch/slug_watch
rename to server/slug_watch/slug_watch
index bdc8c18..78c08d9 100755
--- a/watch/slug_watch
+++ b/server/slug_watch/slug_watch
@@ -15,7 +15,9 @@ import sys
 from contextlib import contextmanager
 from urllib.parse import quote_plus
 
-import Daemon.daemon
+import atexit
+import time
+
 from git_slug.gitconst import EMPTYSHA1, REFREPO, REFFILE
 from git_slug.gitrepo import GitRepo
 
@@ -169,7 +171,97 @@ else:
 handler.setFormatter(logging.Formatter('%(name)s: %(levelname)s %(message)s'))
 logger.addHandler(handler)
 
-class SlugWatch(Daemon.daemon.daemon):
+class Daemon:
+    """Generic UNIX daemon using double-fork mechanism.
+
+    Subclass and override run().  Use start()/stop() to control.
+    """
+    def __init__(self, pidfile):
+        self.pidfile = pidfile
+
+    def daemonize(self):
+        """Daemonize via UNIX double fork."""
+        try:
+            if os.fork() > 0:
+                sys.exit(0)
+        except OSError as err:
+            sys.stderr.write('fork #1 failed: {}\n'.format(err))
+            sys.exit(1)
+
+        os.chdir('/')
+        os.setsid()
+        os.umask(0o033)
+
+        try:
+            if os.fork() > 0:
+                sys.exit(0)
+        except OSError as err:
+            sys.stderr.write('fork #2 failed: {}\n'.format(err))
+            sys.exit(1)
+
+        sys.stdout.flush()
+        sys.stderr.flush()
+        si = open(os.devnull, 'r')
+        so = open(os.devnull, 'a+')
+        se = open(os.devnull, 'a+')
+        os.dup2(si.fileno(), sys.stdin.fileno())
+        os.dup2(so.fileno(), sys.stdout.fileno())
+        os.dup2(se.fileno(), sys.stderr.fileno())
+
+        atexit.register(self._delpid)
+        with open(self.pidfile, 'w+') as f:
+            f.write(str(os.getpid()) + '\n')
+
+    def _delpid(self):
+        os.remove(self.pidfile)
+
+    def start(self):
+        """Start the daemon."""
+        try:
+            with open(self.pidfile, 'r') as pf:
+                pid = int(pf.read().strip())
+        except IOError:
+            pid = None
+        if pid:
+            sys.stderr.write('pidfile {} already exists. Daemon already running?\n'.format(
+                self.pidfile))
+            sys.exit(1)
+        self.daemonize()
+        self.run()
+
+    def stop(self):
+        """Stop the daemon."""
+        try:
+            with open(self.pidfile, 'r') as pf:
+                pid = int(pf.read().strip())
+        except IOError:
+            pid = None
+        if not pid:
+            sys.stderr.write('pidfile {} does not exist. Daemon not running?\n'.format(
+                self.pidfile))
+            return
+        try:
+            while True:
+                os.kill(pid, signal.SIGTERM)
+                time.sleep(0.1)
+        except OSError as err:
+            if 'No such process' in str(err.args):
+                if os.path.exists(self.pidfile):
+                    os.remove(self.pidfile)
+            else:
+                sys.stderr.write(str(err.args) + '\n')
+                sys.exit(1)
+
+    def restart(self):
+        """Restart the daemon."""
+        self.stop()
+        self.start()
+
+    def run(self):
+        """Override in subclass."""
+
+
+class SlugWatch(Daemon):
     def __init__(self, user, pidfile):
         super().__init__(pidfile)
         self.user = user
diff --git a/watch/slug_watch-cron b/server/slug_watch/slug_watch-cron
similarity index 100%
rename from watch/slug_watch-cron
rename to server/slug_watch/slug_watch-cron
diff --git a/watch/slug_watch.init b/server/slug_watch/slug_watch.init
similarity index 100%
rename from watch/slug_watch.init
rename to server/slug_watch/slug_watch.init
diff --git a/watch/slug_watch.service b/server/slug_watch/slug_watch.service
similarity index 100%
rename from watch/slug_watch.service
rename to server/slug_watch/slug_watch.service
diff --git a/watch/slug_watch.sysconfig b/server/slug_watch/slug_watch.sysconfig
similarity index 100%
rename from watch/slug_watch.sysconfig
rename to server/slug_watch/slug_watch.sysconfig
diff --git a/setup.py b/setup.py
index f9d3c51..e3ad592 100644
--- a/setup.py
+++ b/setup.py
@@ -19,8 +19,8 @@ setup(name='git-core-slug',
       author_email='draenog at pld-linux.org',
       url='https://github.com/draenog/slug',
       classifiers=['Programming Language :: Python :: 3'],
-      packages=['git_slug', 'Daemon'],
-      data_files=[('adc/bin', ['adc/trash', 'adc/move'])],
-      scripts=['slug.py', 'watch/slug_watch'],
+      packages=['git_slug'],
+      data_files=[('adc/bin', ['server/adc/trash', 'server/adc/move'])],
+      scripts=['slug.py', 'server/slug_watch/slug_watch'],
       cmdclass={"install_data": post_install}
      )
diff --git a/slug.py b/slug.py
index f12bff9..7454710 100755
--- a/slug.py
+++ b/slug.py
@@ -19,9 +19,6 @@ import glob
 import itertools
 import sys
 import os
-
-# Allow running from source tree or via symlink without installing git_slug.
-sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)))
 import shutil
 import subprocess
 import shlex
diff --git a/tests/test_benchmark.py b/tests/test_benchmark.py
new file mode 100644
index 0000000..a3d14fe
--- /dev/null
+++ b/tests/test_benchmark.py
@@ -0,0 +1,90 @@
+"""Performance benchmarks using pytest-benchmark.
+
+Run:  pytest tests/test_benchmark.py
+      pytest --benchmark-only
+      pytest --benchmark-compare  (compare with saved baseline)
+"""
+
+import os
+import subprocess
+from types import SimpleNamespace
+
+import pytest
+
+import slug
+from git_slug.gitrepo import GitRepo
+
+
+BRANCHES = ['master', 'devel', 'stable', 'release', 'feature/test']
+REF_BRANCHES = ['refs/heads/' + b for b in BRANCHES]
+
+
+def _create_repos(tmp_path, n):
+    """Create n minimal git repos with packed-refs for benchmarking."""
+    repos = []
+    for i in range(n):
+        repo_dir = tmp_path / 'pkg-{:05d}'.format(i)
+        git_dir = repo_dir / '.git'
+        (git_dir / 'refs' / 'remotes' / 'origin').mkdir(parents=True)
+        (git_dir / 'objects').mkdir(parents=True)
+        (git_dir / 'refs' / 'heads').mkdir(parents=True)
+        (git_dir / 'HEAD').write_text('ref: refs/heads/master\n')
+
+        lines = ['# pack-refs with: peeled fully-peeled sorted\n']
+        for b in BRANCHES:
+            sha = 'a' * 39 + str(i % 10)
+            lines.append('{} refs/remotes/origin/{}\n'.format(sha, b))
+        (git_dir / 'packed-refs').write_text(''.join(lines))
+
+        repos.append(str(repo_dir))
+    return repos
+
+
+ at pytest.fixture
+def repos_500(tmp_path):
+    """500 test repos with packed-refs."""
+    return _create_repos(tmp_path, 500)
+
+
+def test_check_remote_cached(benchmark, repos_500):
+    """check_remote() with refs cache — the hot path in update/clone."""
+    repos = repos_500
+
+    def run():
+        for rp in repos:
+            repo = GitRepo(rp)
+            for b in REF_BRANCHES:
+                repo.check_remote(b)
+
+    benchmark(run)
+
+
+def test_find_git_repos(benchmark, repos_500):
+    """find_git_repos() scanning directories."""
+    base = os.path.dirname(repos_500[0])
+    benchmark(slug.find_git_repos, base, ['*'])
+
+
+def test_git_config_get_cached(benchmark, monkeypatch):
+    """git_config_get() with batch-cached config."""
+    slug._pld_config_cache = None
+    monkeypatch.setattr(
+        slug.subprocess,
+        "run",
+        lambda *args, **kwargs: SimpleNamespace(
+            returncode=0,
+            stdout="pld.jobs 8\npld.packagesdir /packages\n",
+        ),
+    )
+    # Warm the cache
+    slug.git_config_get("PLD.jobs")
+
+    keys = ['PLD.packagesdir', 'PLD.jobs', 'PLD.pull-config',
+            'PLD.fetch-config', 'PLD.status-config']
+
+    def run():
+        for _ in range(200):
+            for k in keys:
+                slug.git_config_get(k)
+
+    benchmark(run)
================================================================

---- gitweb:

http://git.pld-linux.org/gitweb.cgi/projects/git-slug.git/commitdiff/4a7e426b8f1a3571094b5dc89412bc49b8f29666



More information about the pld-cvs-commit mailing list