From: Simon Glass <simon.glass@canonical.com>
Add support for storing a GitLab API token in ~/.config/pickman.conf
instead of an environment variable. This allows using a dedicated bot
account for pickman without affecting the user's personal GitLab
credentials.
Config file format:
[gitlab]
token = glpat-xxxxxxxxxxxxxxxxxxxx
This falls back to GITLAB_TOKEN environment variable if config file is
not present.
Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: Simon Glass <simon.glass@canonical.com>
---
tools/pickman/README.rst | 17 ++++++++++---
tools/pickman/ftest.py | 49 +++++++++++++++++++++++++++++++++++++
tools/pickman/gitlab_api.py | 39 ++++++++++++++++++++++++++++-
3 files changed, 101 insertions(+), 4 deletions(-)
@@ -192,9 +192,20 @@ To use the ``-p`` (push) option for GitLab integration, install python-gitlab::
pip install python-gitlab
-You will also need a GitLab API token set in the ``GITLAB_TOKEN`` environment
-variable. See `GitLab Personal Access Tokens`_ for instructions on creating one.
-The token needs ``api`` scope.
+You will also need a GitLab API token. The token can be configured in a config
+file or environment variable. Pickman checks in this order:
+
+1. Config file ``~/.config/pickman.conf``::
+
+ [gitlab]
+ token = glpat-xxxxxxxxxxxxxxxxxxxx
+
+2. ``GITLAB_TOKEN`` environment variable
+3. ``GITLAB_API_TOKEN`` environment variable
+
+See `GitLab Personal Access Tokens`_ for instructions on creating a token.
+The token needs ``api`` scope. Using a dedicated bot account for pickman is
+recommended.
.. _GitLab Personal Access Tokens:
https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html
@@ -1431,6 +1431,55 @@ class TestCheckAvailable(unittest.TestCase):
self.assertTrue(result)
+class TestConfigFile(unittest.TestCase):
+ """Tests for config file support."""
+
+ def setUp(self):
+ """Set up test fixtures."""
+ self.config_dir = tempfile.mkdtemp()
+ self.config_file = os.path.join(self.config_dir, 'pickman.conf')
+
+ def tearDown(self):
+ """Clean up test fixtures."""
+ shutil.rmtree(self.config_dir)
+
+ def test_get_token_from_config(self):
+ """Test getting token from config file."""
+ with open(self.config_file, 'w', encoding='utf-8') as fhandle:
+ fhandle.write('[gitlab]\ntoken = test-config-token\n')
+
+ with mock.patch.object(gitlab_api, 'CONFIG_FILE', self.config_file):
+ token = gitlab_api.get_token()
+ self.assertEqual(token, 'test-config-token')
+
+ def test_get_token_fallback_to_env(self):
+ """Test falling back to environment variable."""
+ # Config file doesn't exist
+ with mock.patch.object(gitlab_api, 'CONFIG_FILE', '/nonexistent/path'):
+ with mock.patch.dict(os.environ, {'GITLAB_TOKEN': 'env-token'}):
+ token = gitlab_api.get_token()
+ self.assertEqual(token, 'env-token')
+
+ def test_get_token_config_missing_section(self):
+ """Test config file without gitlab section."""
+ with open(self.config_file, 'w', encoding='utf-8') as fhandle:
+ fhandle.write('[other]\nkey = value\n')
+
+ with mock.patch.object(gitlab_api, 'CONFIG_FILE', self.config_file):
+ with mock.patch.dict(os.environ, {'GITLAB_TOKEN': 'env-token'}):
+ token = gitlab_api.get_token()
+ self.assertEqual(token, 'env-token')
+
+ def test_get_config_value(self):
+ """Test get_config_value function."""
+ with open(self.config_file, 'w', encoding='utf-8') as fhandle:
+ fhandle.write('[section1]\nkey1 = value1\n')
+
+ with mock.patch.object(gitlab_api, 'CONFIG_FILE', self.config_file):
+ value = gitlab_api.get_config_value('section1', 'key1')
+ self.assertEqual(value, 'value1')
+
+
class TestUpdateMrDescription(unittest.TestCase):
"""Tests for update_mr_description function."""
@@ -6,6 +6,7 @@
"""GitLab integration for pickman - push branches and create merge requests."""
from collections import namedtuple
+import configparser
import os
import re
import sys
@@ -50,12 +51,48 @@ def check_available():
return True
+CONFIG_FILE = os.path.expanduser('~/.config/pickman.conf')
+
+
+def get_config_value(section, key):
+ """Get a value from the pickman config file
+
+ Args:
+ section (str): Config section name
+ key (str): Config key name
+
+ Returns:
+ str: Value or None if not found
+ """
+ if not os.path.exists(CONFIG_FILE):
+ return None
+
+ config = configparser.ConfigParser()
+ config.read(CONFIG_FILE)
+
+ try:
+ return config.get(section, key)
+ except (configparser.NoSectionError, configparser.NoOptionError):
+ return None
+
+
def get_token():
- """Get GitLab API token from environment
+ """Get GitLab API token from config file or environment
+
+ Checks in order:
+ 1. Config file (~/.config/pickman.conf) [gitlab] token
+ 2. GITLAB_TOKEN environment variable
+ 3. GITLAB_API_TOKEN environment variable
Returns:
str: Token or None if not set
"""
+ # Try config file first
+ token = get_config_value('gitlab', 'token')
+ if token:
+ return token
+
+ # Fall back to environment variables
return os.environ.get('GITLAB_TOKEN') or os.environ.get('GITLAB_API_TOKEN')