cgitize 6.0.5__tar.gz → 8.0.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 (69) hide show
  1. {cgitize-6.0.5 → cgitize-8.0.0}/.github/workflows/ci.yml +10 -11
  2. {cgitize-6.0.5 → cgitize-8.0.0}/.github/workflows/maintenance.yml +3 -6
  3. {cgitize-6.0.5 → cgitize-8.0.0}/.pylintrc +5 -1
  4. cgitize-8.0.0/.python-version +1 -0
  5. {cgitize-6.0.5 → cgitize-8.0.0}/DEVELOPMENT.md +3 -3
  6. {cgitize-6.0.5 → cgitize-8.0.0}/PKG-INFO +1 -1
  7. {cgitize-6.0.5 → cgitize-8.0.0}/cgitize/bitbucket.py +5 -5
  8. {cgitize-6.0.5 → cgitize-8.0.0}/cgitize/config.py +25 -19
  9. {cgitize-6.0.5 → cgitize-8.0.0}/cgitize/github.py +14 -4
  10. {cgitize-6.0.5 → cgitize-8.0.0}/cgitize/gitlab.py +4 -3
  11. {cgitize-6.0.5 → cgitize-8.0.0}/cgitize/repo.py +78 -7
  12. {cgitize-6.0.5 → cgitize-8.0.0}/cgitize/utils.py +9 -5
  13. {cgitize-6.0.5 → cgitize-8.0.0}/cgitize.egg-info/PKG-INFO +1 -1
  14. {cgitize-6.0.5 → cgitize-8.0.0}/docker/README.md +2 -2
  15. {cgitize-6.0.5 → cgitize-8.0.0}/docs/api.md +6 -4
  16. {cgitize-6.0.5 → cgitize-8.0.0}/docs/auth.md +3 -5
  17. {cgitize-6.0.5 → cgitize-8.0.0}/examples/cgitize.toml +26 -14
  18. {cgitize-6.0.5 → cgitize-8.0.0}/requirements.txt +6 -6
  19. {cgitize-6.0.5 → cgitize-8.0.0}/test/integration/example/test.sh +92 -0
  20. {cgitize-6.0.5 → cgitize-8.0.0}/test/set_credentials.sh +3 -1
  21. {cgitize-6.0.5 → cgitize-8.0.0}/test/unit/test_bitbucket.py +20 -1
  22. {cgitize-6.0.5 → cgitize-8.0.0}/test/unit/test_github.py +2 -0
  23. {cgitize-6.0.5 → cgitize-8.0.0}/test/unit/test_gitlab.py +2 -0
  24. cgitize-6.0.5/.python-version +0 -1
  25. {cgitize-6.0.5 → cgitize-8.0.0}/.dockerignore +0 -0
  26. {cgitize-6.0.5 → cgitize-8.0.0}/.gitattributes +0 -0
  27. {cgitize-6.0.5 → cgitize-8.0.0}/.github/dependabot.yml +0 -0
  28. {cgitize-6.0.5 → cgitize-8.0.0}/.gitignore +0 -0
  29. {cgitize-6.0.5 → cgitize-8.0.0}/LICENSE.txt +0 -0
  30. {cgitize-6.0.5 → cgitize-8.0.0}/Makefile +0 -0
  31. {cgitize-6.0.5 → cgitize-8.0.0}/README.md +0 -0
  32. {cgitize-6.0.5 → cgitize-8.0.0}/cgitize/__init__.py +0 -0
  33. {cgitize-6.0.5 → cgitize-8.0.0}/cgitize/cgit.py +0 -0
  34. {cgitize-6.0.5 → cgitize-8.0.0}/cgitize/git.py +0 -0
  35. {cgitize-6.0.5 → cgitize-8.0.0}/cgitize/header.py +0 -0
  36. {cgitize-6.0.5 → cgitize-8.0.0}/cgitize/main.py +0 -0
  37. {cgitize-6.0.5 → cgitize-8.0.0}/cgitize/version.py +0 -0
  38. {cgitize-6.0.5 → cgitize-8.0.0}/cgitize.egg-info/SOURCES.txt +0 -0
  39. {cgitize-6.0.5 → cgitize-8.0.0}/cgitize.egg-info/dependency_links.txt +0 -0
  40. {cgitize-6.0.5 → cgitize-8.0.0}/cgitize.egg-info/entry_points.txt +0 -0
  41. {cgitize-6.0.5 → cgitize-8.0.0}/cgitize.egg-info/requires.txt +0 -0
  42. {cgitize-6.0.5 → cgitize-8.0.0}/cgitize.egg-info/top_level.txt +0 -0
  43. {cgitize-6.0.5 → cgitize-8.0.0}/docker/Dockerfile +0 -0
  44. {cgitize-6.0.5 → cgitize-8.0.0}/docker/cgitize.sh +0 -0
  45. {cgitize-6.0.5 → cgitize-8.0.0}/docker/frontend/Dockerfile +0 -0
  46. {cgitize-6.0.5 → cgitize-8.0.0}/docker/frontend/cmd.sh +0 -0
  47. {cgitize-6.0.5 → cgitize-8.0.0}/docker/frontend/etc/cgit/common +0 -0
  48. {cgitize-6.0.5 → cgitize-8.0.0}/docker/frontend/etc/cgitrc +0 -0
  49. {cgitize-6.0.5 → cgitize-8.0.0}/docker/frontend/etc/nginx/conf.d/default.conf +0 -0
  50. {cgitize-6.0.5 → cgitize-8.0.0}/docker/frontend/usr/lib/cgit/filters/about-formatting.sh +0 -0
  51. {cgitize-6.0.5 → cgitize-8.0.0}/docker/frontend/usr/lib/cgit/filters/html-converters/.gitattributes +0 -0
  52. {cgitize-6.0.5 → cgitize-8.0.0}/docker/frontend/usr/lib/cgit/filters/html-converters/md2html +0 -0
  53. {cgitize-6.0.5 → cgitize-8.0.0}/docker/get_output_dir.py +0 -0
  54. {cgitize-6.0.5 → cgitize-8.0.0}/docker/in_cron.sh +0 -0
  55. {cgitize-6.0.5 → cgitize-8.0.0}/docker-compose.yml +0 -0
  56. {cgitize-6.0.5 → cgitize-8.0.0}/examples/docker/cgitize.toml +0 -0
  57. {cgitize-6.0.5 → cgitize-8.0.0}/prelude.mk +0 -0
  58. {cgitize-6.0.5 → cgitize-8.0.0}/pyproject.toml +0 -0
  59. {cgitize-6.0.5 → cgitize-8.0.0}/setup.cfg +0 -0
  60. {cgitize-6.0.5 → cgitize-8.0.0}/test/__init__.py +0 -0
  61. {cgitize-6.0.5 → cgitize-8.0.0}/test/integration/docker/.dockerignore +0 -0
  62. {cgitize-6.0.5 → cgitize-8.0.0}/test/integration/docker/cgitize/.gitignore +0 -0
  63. {cgitize-6.0.5 → cgitize-8.0.0}/test/integration/docker/cgitize/etc/cgitize.toml +0 -0
  64. {cgitize-6.0.5 → cgitize-8.0.0}/test/integration/docker/docker-compose.yml +0 -0
  65. {cgitize-6.0.5 → cgitize-8.0.0}/test/integration/docker/git_server/Dockerfile +0 -0
  66. {cgitize-6.0.5 → cgitize-8.0.0}/test/integration/docker/git_server/setup_repo.sh +0 -0
  67. {cgitize-6.0.5 → cgitize-8.0.0}/test/integration/docker/test.sh +0 -0
  68. {cgitize-6.0.5 → cgitize-8.0.0}/test/integration/local/test.sh +0 -0
  69. {cgitize-6.0.5 → cgitize-8.0.0}/test/unit/__init__.py +0 -0
@@ -21,19 +21,18 @@ jobs:
21
21
  uses: psf/black@stable
22
22
 
23
23
  test_local:
24
- needs: [lint]
25
24
  runs-on: ubuntu-latest
26
25
  strategy:
27
26
  matrix:
28
27
  python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
29
28
  name: 'Test / ${{ matrix.python-version }}'
30
29
  env:
31
- CGITIZE_GITHUB_USERNAME: cgitize-test
30
+ CGITIZE_GITHUB_USERNAME: '${{ secrets.CGITIZE_GITHUB_USERNAME }}'
32
31
  CGITIZE_GITHUB_TOKEN: '${{ secrets.CGITIZE_GITHUB_TOKEN }}'
33
- CGITIZE_BITBUCKET_USERNAME: cgitize-test
32
+ CGITIZE_BITBUCKET_EMAIL: '${{ secrets.CGITIZE_BITBUCKET_EMAIL }}'
34
33
  CGITIZE_BITBUCKET_TOKEN: '${{ secrets.CGITIZE_BITBUCKET_TOKEN }}'
35
- CGITIZE_GITLAB_USERNAME: cgitize-test
36
- CGITIZE_GITLAB_TOKEN: '${{ secrets.CGITIZE_GITLAB_TOKEN}}'
34
+ CGITIZE_GITLAB_USERNAME: '${{ secrets.CGITIZE_GITLAB_USERNAME }}'
35
+ CGITIZE_GITLAB_TOKEN: '${{ secrets.CGITIZE_GITLAB_TOKEN }}'
37
36
  steps:
38
37
  - name: Checkout
39
38
  uses: actions/checkout@v6
@@ -65,12 +64,12 @@ jobs:
65
64
  runs-on: ubuntu-latest
66
65
  name: 'Test / example config'
67
66
  env:
68
- CGITIZE_GITHUB_USERNAME: cgitize-test
67
+ CGITIZE_GITHUB_USERNAME: '${{ secrets.CGITIZE_GITHUB_USERNAME }}'
69
68
  CGITIZE_GITHUB_TOKEN: '${{ secrets.CGITIZE_GITHUB_TOKEN }}'
70
- CGITIZE_BITBUCKET_USERNAME: cgitize-test
69
+ CGITIZE_BITBUCKET_EMAIL: '${{ secrets.CGITIZE_BITBUCKET_EMAIL }}'
71
70
  CGITIZE_BITBUCKET_TOKEN: '${{ secrets.CGITIZE_BITBUCKET_TOKEN }}'
72
- CGITIZE_GITLAB_USERNAME: cgitize-test
73
- CGITIZE_GITLAB_TOKEN: '${{ secrets.CGITIZE_GITLAB_TOKEN}}'
71
+ CGITIZE_GITLAB_USERNAME: '${{ secrets.CGITIZE_GITLAB_USERNAME }}'
72
+ CGITIZE_GITLAB_TOKEN: '${{ secrets.CGITIZE_GITLAB_TOKEN }}'
74
73
  steps:
75
74
  - name: Checkout
76
75
  uses: actions/checkout@v6
@@ -88,7 +87,7 @@ jobs:
88
87
  run: make test/example
89
88
 
90
89
  publish_docker:
91
- needs: [test_local, test_example_config, test_docker]
90
+ needs: [lint, test_local, test_example_config, test_docker]
92
91
  runs-on: ubuntu-latest
93
92
  strategy:
94
93
  matrix:
@@ -149,7 +148,7 @@ jobs:
149
148
  tags: '${{ steps.meta.outputs.tags }}'
150
149
 
151
150
  publish_pypi:
152
- needs: [test_local, test_example_config, test_docker, publish_docker]
151
+ needs: [publish_docker]
153
152
  runs-on: ubuntu-latest
154
153
  name: 'Publish / PyPI'
155
154
  steps:
@@ -16,8 +16,6 @@ jobs:
16
16
  with:
17
17
  # I think that by default, pushes don't trigger other workflows, and
18
18
  # you need a separate token for that: https://github.com/orgs/community/discussions/37103
19
- #
20
- # Re-using the Jekyll projects token here BTW:
21
19
  token: '${{ secrets.GH_TOKEN }}'
22
20
  - name: Set up ssh-agent
23
21
  uses: webfactory/ssh-agent@v0.10.0
@@ -27,11 +25,10 @@ jobs:
27
25
  uses: actions/setup-python@v6
28
26
  with:
29
27
  python-version-file: .python-version
30
- - name: Run maintenance
31
- id: maintenance
28
+ - name: Configure git
32
29
  run: |
33
30
  # See https://github.com/actions/checkout#push-a-commit-using-the-built-in-token
34
31
  git config user.name "github-actions[bot]"
35
32
  git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
36
-
37
- make maintenance
33
+ - name: Run maintenance
34
+ run: make maintenance
@@ -7,4 +7,8 @@ init-hook=
7
7
 
8
8
  [MESSAGES CONTROL]
9
9
 
10
- disable=invalid-name,missing-docstring,too-few-public-methods,too-many-arguments
10
+ disable=
11
+ invalid-name,
12
+ missing-docstring,
13
+ too-few-public-methods,
14
+ too-many-arguments,
@@ -0,0 +1 @@
1
+ 3.13.13
@@ -18,11 +18,11 @@ test.
18
18
  You need to set the following environment variables for the authenticated
19
19
  access tests to work:
20
20
 
21
- * `CGITIZE_BITBUCKET_USERNAME`: `cgitize-test`
21
+ * `CGITIZE_BITBUCKET_EMAIL`
22
22
  * `CGITIZE_BITBUCKET_TOKEN`
23
- * `CGITIZE_GITHUB_USERNAME`: `cgitize-test`
23
+ * `CGITIZE_GITHUB_USERNAME`
24
24
  * `CGITIZE_GITHUB_TOKEN`
25
- * `CGITIZE_GITLAB_USERNAME`: `cgitize-test`
25
+ * `CGITIZE_GITLAB_USERNAME`
26
26
  * `CGITIZE_GITLAB_TOKEN`
27
27
 
28
28
  You can run `. test/set_credentials.sh`; it'll ask you to fill in the secrets.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cgitize
3
- Version: 6.0.5
3
+ Version: 8.0.0
4
4
  Summary: Self-host your repositories using cgit
5
5
  Author-email: Egor Tensin <egor@tensin.name>
6
6
  License-Expression: MIT
@@ -8,12 +8,12 @@ import logging
8
8
  from atlassian.bitbucket.cloud import Cloud
9
9
  from requests.exceptions import HTTPError
10
10
 
11
- from cgitize.repo import Repo
11
+ from cgitize.repo import Repo, Visibility
12
12
 
13
13
 
14
14
  class Bitbucket:
15
- def __init__(self, username=None, password=None):
16
- self._impl = Cloud(username=username, password=password, cloud=True)
15
+ def __init__(self, email=None, token=None):
16
+ self._impl = Cloud(username=email, password=token, cloud=True)
17
17
 
18
18
  def get_repo(self, repo):
19
19
  try:
@@ -28,10 +28,10 @@ class Bitbucket:
28
28
  logging.error("Couldn't fetch repository: %s", repo.id)
29
29
  raise
30
30
 
31
- def get_user_repos(self, user):
31
+ def get_user_repos(self, user, visibility=Visibility.ALL):
32
32
  try:
33
33
  workspace = self._impl.workspaces.get(user.name)
34
- yield from workspace.repositories.each()
34
+ yield from workspace.repositories.each(q=visibility.to_bitbucket_arg())
35
35
  except HTTPError:
36
36
  logging.error("Couldn't fetch user repositories: %s", user.name)
37
37
  raise
@@ -5,13 +5,14 @@
5
5
 
6
6
  from abc import ABC, abstractmethod
7
7
  import os
8
+ from urllib.parse import quote
8
9
 
9
10
  import tomli
10
11
 
11
12
  from cgitize.bitbucket import Bitbucket
12
13
  from cgitize.github import GitHub
13
14
  from cgitize.gitlab import GitLab
14
- from cgitize.repo import Repo
15
+ from cgitize.repo import Repo, Visibility
15
16
 
16
17
 
17
18
  class Section:
@@ -74,7 +75,8 @@ class ServiceSection(Section, ABC):
74
75
  yield api.convert_repo(api.get_repo(r), cfg, r.dir)
75
76
 
76
77
  def _enum_user_repositories(self, cfg, api, user):
77
- for r in api.get_user_repos(user):
78
+ visibility = Visibility.from_config(user.public_repos, user.private_repos)
79
+ for r in api.get_user_repos(user, visibility=visibility):
78
80
  r = api.convert_repo(r, cfg, user.dir)
79
81
  if r.name in user.skip:
80
82
  continue
@@ -109,7 +111,7 @@ class GitHubSection(ServiceSection):
109
111
 
110
112
  @property
111
113
  def url_auth(self):
112
- return self.token
114
+ return (self.token, None)
113
115
 
114
116
  def _enum_org_repositories(self, cfg, api, org):
115
117
  for repo in api.get_org_repos(org):
@@ -134,12 +136,6 @@ class GitHubSection(ServiceSection):
134
136
  return GitHub(username, token)
135
137
 
136
138
 
137
- def two_part_url_auth(username, password):
138
- if username is None or password is None:
139
- return None
140
- return f'{username}:{password}'
141
-
142
-
143
139
  class BitbucketSection(ServiceSection):
144
140
  @property
145
141
  def token(self):
@@ -147,11 +143,11 @@ class BitbucketSection(ServiceSection):
147
143
 
148
144
  @property
149
145
  def username(self):
150
- return self._get_config_or_env('username', 'CGITIZE_BITBUCKET_USERNAME')
146
+ return self._get_config_or_env('email', 'CGITIZE_BITBUCKET_EMAIL')
151
147
 
152
148
  @property
153
149
  def url_auth(self):
154
- return two_part_url_auth(self.username, self.token)
150
+ return (self.username, self.token)
155
151
 
156
152
  def connect_to_service(self):
157
153
  username = self.username
@@ -174,7 +170,7 @@ class GitLabSection(ServiceSection):
174
170
 
175
171
  @property
176
172
  def url_auth(self):
177
- return two_part_url_auth(self.username, self.token)
173
+ return (self.username, self.token)
178
174
 
179
175
  def connect_to_service(self):
180
176
  username = self.username
@@ -201,12 +197,7 @@ class RepositoriesSection(Section):
201
197
  return self.impl.values()
202
198
 
203
199
 
204
- class User:
205
- def __init__(self, impl):
206
- if 'name' not in impl:
207
- raise ValueError("every user must have a 'name'")
208
- self._impl = impl
209
-
200
+ class Entity(ABC):
210
201
  @property
211
202
  def name(self):
212
203
  return self._impl['name']
@@ -220,7 +211,22 @@ class User:
220
211
  return self._impl.get('skip', [])
221
212
 
222
213
 
223
- class Org(User):
214
+ class User(Entity):
215
+ def __init__(self, impl):
216
+ if 'name' not in impl:
217
+ raise ValueError("every user must have a 'name'")
218
+ self._impl = impl
219
+
220
+ @property
221
+ def public_repos(self):
222
+ return self._impl.get('public', True)
223
+
224
+ @property
225
+ def private_repos(self):
226
+ return self._impl.get('private', False)
227
+
228
+
229
+ class Org(Entity):
224
230
  def __init__(self, impl):
225
231
  if 'name' not in impl:
226
232
  raise ValueError("every organization must have a 'name'")
@@ -7,7 +7,7 @@ import logging
7
7
 
8
8
  from github import Auth, Github, GithubException
9
9
 
10
- from cgitize.repo import Repo
10
+ from cgitize.repo import Repo, Visibility
11
11
 
12
12
 
13
13
  class GitHub:
@@ -25,14 +25,24 @@ class GitHub:
25
25
  logging.error("Couldn't fetch repository: %s", repo.id)
26
26
  raise
27
27
 
28
- def get_user_repos(self, user):
28
+ def get_user_repos(self, user, visibility=Visibility.ALL):
29
+ visibility = visibility.to_github_arg()
30
+
31
+ # Not actually certain how it works, but just passing None as the
32
+ # named visibility parameter fails with an internal assert error. I
33
+ # honestly thought that that passing parameters like that and as a
34
+ # None makes no difference.
35
+ kwargs = {}
36
+ if visibility is not None:
37
+ kwargs['visibility'] = visibility
38
+
29
39
  try:
30
40
  if user.name == self._username:
31
41
  # To get private repositories, get_user() must be called
32
42
  # without arguments:
33
- return self._impl.get_user().get_repos(affiliation='owner')
43
+ return self._impl.get_user().get_repos(affiliation='owner', **kwargs)
34
44
  else:
35
- return self._impl.get_user(user.name).get_repos()
45
+ return self._impl.get_user(user.name).get_repos(**kwargs)
36
46
  except GithubException:
37
47
  logging.error("Couldn't fetch user repositories: %s", user.name)
38
48
  raise
@@ -8,7 +8,7 @@ import logging
8
8
  from gitlab import Gitlab
9
9
  from gitlab.exceptions import GitlabGetError
10
10
 
11
- from cgitize.repo import Repo
11
+ from cgitize.repo import Repo, Visibility
12
12
 
13
13
 
14
14
  class GitLab:
@@ -22,7 +22,8 @@ class GitLab:
22
22
  logging.error("Couldn't fetch repository: %s", repo.id)
23
23
  raise
24
24
 
25
- def get_user_repos(self, user):
25
+ def get_user_repos(self, user, visibility=Visibility.ALL):
26
+ visibility = visibility.to_gitlab_arg()
26
27
  try:
27
28
  # Strictly speaking, GitLab supports the /users/:username/projects
28
29
  # endpoint, which means you shouldn't need to fetch the user first,
@@ -32,7 +33,7 @@ class GitLab:
32
33
  raise RuntimeError(f"Couldn't find GitLab user: {user.name}")
33
34
  assert len(users) == 1
34
35
  user = users[0]
35
- return user.projects.list()
36
+ return user.projects.list(visibility=visibility)
36
37
  except GitlabGetError:
37
38
  logging.error("Couldn't fetch user repositories: %s", user.name)
38
39
  raise
@@ -3,11 +3,64 @@
3
3
  # For details, see https://github.com/egor-tensin/cgitize.
4
4
  # Distributed under the MIT License.
5
5
 
6
+ from enum import Enum
6
7
  import os.path
7
8
 
8
9
  from cgitize.utils import url_remove_auth, url_replace_auth
9
10
 
10
11
 
12
+ class Visibility(Enum):
13
+ ALL = 'all'
14
+ PUBLIC = 'public'
15
+ PRIVATE = 'private'
16
+
17
+ def __str__(self):
18
+ return self.name
19
+
20
+ @staticmethod
21
+ def from_config(public, private):
22
+ if public and private:
23
+ return Visibility.ALL
24
+ if not public and not private:
25
+ raise RuntimeError(
26
+ 'you need to fetch either public or private repos, or both'
27
+ )
28
+ if public:
29
+ return Visibility.PUBLIC
30
+ return Visibility.PRIVATE
31
+
32
+ def to_bitbucket_arg(self):
33
+ # https://developer.atlassian.com/cloud/bitbucket/rest/intro/#filtering
34
+ # https://developer.atlassian.com/cloud/bitbucket/rest/api-group-repositories/#api-group-repositories
35
+ if self is Visibility.ALL:
36
+ return None
37
+ if self is Visibility.PUBLIC:
38
+ return 'is_private=False'
39
+ if self is Visibility.PRIVATE:
40
+ return 'is_private=True'
41
+ raise NotImplementedError(f'not implemented visibility level: {self}')
42
+
43
+ def to_github_arg(self):
44
+ # https://docs.github.com/en/rest/repos/repos?apiVersion=2026-03-10#list-repositories-for-the-authenticated-user
45
+ if self is Visibility.ALL:
46
+ return None
47
+ if self is Visibility.PUBLIC:
48
+ return 'public'
49
+ if self is Visibility.PRIVATE:
50
+ return 'private'
51
+ raise NotImplementedError(f'not implemented visibility level: {self}')
52
+
53
+ def to_gitlab_arg(self):
54
+ # https://python-gitlab.readthedocs.io/en/stable/gl_objects/projects.html#projects
55
+ if self is Visibility.ALL:
56
+ return None
57
+ if self is Visibility.PUBLIC:
58
+ return 'public'
59
+ if self is Visibility.PRIVATE:
60
+ return 'private'
61
+ raise NotImplementedError(f'not implemented visibility level: {self}')
62
+
63
+
11
64
  class Repo:
12
65
  @staticmethod
13
66
  def from_config(src, config):
@@ -34,8 +87,12 @@ class Repo:
34
87
 
35
88
  https_url = src.clone_url
36
89
  ssh_url = src.ssh_url
37
- clone_url = ssh_url if config.main.clone_via_ssh else https_url
38
- url_auth = None if config.main.clone_via_ssh else config.github.url_auth
90
+
91
+ clone_url = ssh_url
92
+ url_auth = None
93
+ if not config.main.clone_via_ssh:
94
+ clone_url = https_url
95
+ url_auth = config.github.url_auth
39
96
 
40
97
  return Repo(
41
98
  name,
@@ -60,7 +117,7 @@ class Repo:
60
117
  if len(https_urls) != 1:
61
118
  raise RuntimeError(f"no https:// clone URL for repository '{name}'?!")
62
119
  # Bitbucket leaves the username in the URL... Sigh.
63
- https_url = url_remove_auth(https_urls[0]['href'])
120
+ api_auth, https_url = url_remove_auth(https_urls[0]['href'])
64
121
 
65
122
  ssh_urls = [
66
123
  link for link in src.data['links']['clone'] if link['name'] == 'ssh'
@@ -69,8 +126,18 @@ class Repo:
69
126
  raise RuntimeError(f"no ssh:// clone URL for repository '{name}'?!")
70
127
  ssh_url = ssh_urls[0]['href']
71
128
 
72
- clone_url = ssh_url if config.main.clone_via_ssh else https_url
73
- url_auth = None if config.main.clone_via_ssh else config.bitbucket.url_auth
129
+ clone_url = ssh_url
130
+ url_auth = None
131
+ if not config.main.clone_via_ssh:
132
+ clone_url = https_url
133
+ url_auth = config.bitbucket.url_auth
134
+ # Bitbucket, since deprecating app passwords, now requires that
135
+ # users authenticate with their Atlassian email & API token.
136
+ # Unfortunately (sigh...), if you want to clone a repo via HTTPS,
137
+ # you can't use the very same email + token combo in the URL, you
138
+ # have to use your username (double sigh...). Luckily, for some
139
+ # reason, Bitbucket leaves the relevant username in the API response.
140
+ url_auth = api_auth[0], url_auth[1]
74
141
 
75
142
  return Repo(
76
143
  name,
@@ -91,8 +158,12 @@ class Repo:
91
158
 
92
159
  https_url = src.http_url_to_repo
93
160
  ssh_url = src.ssh_url_to_repo
94
- clone_url = ssh_url if config.main.clone_via_ssh else https_url
95
- url_auth = None if config.main.clone_via_ssh else config.gitlab.url_auth
161
+
162
+ clone_url = ssh_url
163
+ url_auth = None
164
+ if not config.main.clone_via_ssh:
165
+ clone_url = https_url
166
+ url_auth = config.gitlab.url_auth
96
167
 
97
168
  return Repo(
98
169
  name,
@@ -9,7 +9,7 @@ import os
9
9
  import stat
10
10
  import subprocess
11
11
  import sys
12
- from urllib.parse import urlsplit, urlunsplit
12
+ from urllib.parse import quote, urlsplit, urlunsplit
13
13
 
14
14
 
15
15
  @contextmanager
@@ -97,11 +97,12 @@ def protected_file(path):
97
97
  os.unlink(path)
98
98
 
99
99
 
100
- def url_replace_auth(url, username, password=None):
100
+ def url_replace_auth(url, auth):
101
+ username, password = auth
101
102
  parts = urlsplit(url)
102
- netloc = username
103
+ netloc = quote(username)
103
104
  if password is not None:
104
- netloc += f':{password}'
105
+ netloc += f':{quote(password)}'
105
106
  netloc += f'@{parts.hostname}'
106
107
  if parts.port is not None:
107
108
  netloc += f':{parts.port}'
@@ -111,6 +112,9 @@ def url_replace_auth(url, username, password=None):
111
112
 
112
113
  def url_remove_auth(url):
113
114
  parts = urlsplit(url)
115
+ auth = parts.username, parts.password
114
116
  netloc = parts.hostname
117
+ if parts.port is not None:
118
+ netloc += f':{parts.port}'
115
119
  parts = parts._replace(netloc=netloc)
116
- return urlunsplit(parts)
120
+ return auth, urlunsplit(parts)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cgitize
3
- Version: 6.0.5
3
+ Version: 8.0.0
4
4
  Summary: Self-host your repositories using cgit
5
5
  Author-email: Egor Tensin <egor@tensin.name>
6
6
  License-Expression: MIT
@@ -48,13 +48,13 @@ Here's an example docker-compose.yml file:
48
48
  SCHEDULE: '0 */3 * * *'
49
49
  # Set CGITIZE_{GITHUB,BITBUCKET,GITLAB}_{USERNAME,TOKEN} variables
50
50
  # here or in the config file.
51
- image: egortensin/cgitize:6
51
+ image: egortensin/cgitize:8
52
52
  restart: unless-stopped
53
53
  volumes:
54
54
  - ./example.toml:/etc/cgitize/cgitize.toml:ro
55
55
  - /srv/volumes/cgitize:/mnt/cgitize
56
56
  frontend:
57
- image: egortensin/cgitize-frontend:6
57
+ image: egortensin/cgitize-frontend:8
58
58
  ports:
59
59
  - '127.0.0.1:80:80'
60
60
  restart: unless-stopped
@@ -4,10 +4,9 @@ unfortunately, peculiar in their own way.
4
4
  Tokens
5
5
  ------
6
6
 
7
- All of the forges support access tokens, of course (although they're called
8
- "app passwords" on Bitbucket). They can be used to do `git clone https://`
9
- with authentication info in the URL. For GitHub, it's enough to just specify
10
- the token:
7
+ All of the forges support access tokens, of course. They can be used to do
8
+ `git clone https://` with authentication info in the URL. For GitHub, it's
9
+ enough to just specify the token:
11
10
 
12
11
  git clone https://TOKEN@github.com/USER/REPO.git
13
12
 
@@ -16,6 +15,9 @@ token in the URL:
16
15
 
17
16
  git clone https://USER:TOKEN@bitbucket.org/USER/REPO.git
18
17
 
18
+ (For Bitbucket, since they deprecated their "app passwords", you need to
19
+ specify not the username, but rather the Atlassian account email.)
20
+
19
21
  Terminology
20
22
  -----------
21
23
 
@@ -22,11 +22,9 @@ When using HTTPS, there're multiple options to access the private repositories.
22
22
 
23
23
  2. Use OAuth. I thought this was too complicated to implement quickly.
24
24
 
25
- 3. Use per-application passwords. This option is pretty cool. Both GitHub (in
26
- the form of "personal access tokens") and Bitbucket (calls them "app
27
- passwords") support creating one-purpose passwords that are supposed to be
28
- used by a single app only. They allow to bypass the 2FA also. Let's do
29
- this!
25
+ 3. Use application access tokens. This option is pretty cool. Both GitHub
26
+ and Bitbucket support creating read-only tokens. They allow to bypass the
27
+ 2FA also. Let's do this!
30
28
 
31
29
  Passing the access tokens to Git is tricky though. First, simply including
32
30
  them in the clone URL would be incredibly stupid, since that URL is going to be
@@ -7,8 +7,8 @@ output_dir = "/tmp/cgitize"
7
7
  # repository's directory path.
8
8
  clone_url = "https://yourhost.com/git/{repo}"
9
9
 
10
- # Clones via SSH by default.
11
- clone_via_ssh = false
10
+ # Clones via SSH by default (instead of Git-over-HTTPS).
11
+ clone_via_ssh = true
12
12
 
13
13
  # Unless this is specified, cgit is going to derive the owner from the
14
14
  # repository's directory ownership.
@@ -19,8 +19,10 @@ owner = "Your Name"
19
19
 
20
20
  [github]
21
21
  # If some of the repositories hosted on GitHub are private (or if you hit the
22
- # request rate limit), use "access tokens". Set in the config or using the
23
- # CGITIZE_GITHUB_TOKEN environment variable.
22
+ # request rate limit), use a personal access token. Set in the config or using
23
+ # the CGITIZE_GITHUB_TOKEN environment variable.
24
+ # Required scopes:
25
+ # * repo
24
26
  #token = "XXX"
25
27
  # Set the username that owns the token here or using the environment variable
26
28
  # CGITIZE_GITHUB_USERNAME.
@@ -34,6 +36,10 @@ dir = "github-dir"
34
36
  # Repositories to skip:
35
37
  skip = ["repo1", "repo2"]
36
38
 
39
+ # The defaults.
40
+ public = true
41
+ private = false
42
+
37
43
  [github.organizations.goauthentik]
38
44
  # For GitHub, you can also specify organizations. The settings are similar to
39
45
  # those for users. "goauthentik" is a random organization.
@@ -52,20 +58,23 @@ dir = "python"
52
58
  # Bitbucket
53
59
  # =========
54
60
 
55
- # Bitbucket settings are similar to those for GitHub.
56
-
57
61
  [bitbucket]
58
- # Access tokens are called "app passwords" on Bitbucket.
59
- # The environment variable is CGITIZE_BITBUCKET_TOKEN.
62
+ # Atlassian API token (environment variable CGITIZE_BITBUCKET_TOKEN).
63
+ # Required scopes:
64
+ # * read:workspace:bitbucket
65
+ # * read:repository:bitbucket
60
66
  #token = "XXX"
61
- # The environment variable is CGITIZE_BITBUCKET_USERNAME.
62
- #username = "your-username"
67
+ # Atlassian account email address (environment variable CGITIZE_BITBUCKET_EMAIL).
68
+ #email = "your-email"
63
69
 
64
70
  [bitbucket.users.me]
65
71
  name = "cgitize-test-workspace"
66
72
  dir = "bitbucket-dir"
67
73
  #skip = ["repo1", "repo2"]
68
74
 
75
+ public = true
76
+ private = false
77
+
69
78
  # Some random repositories hosted on Bitbucket:
70
79
  [bitbucket.repositories.cef]
71
80
  id = "chromiumembedded/cef"
@@ -76,12 +85,12 @@ id = "berkeleylab/upc-runtime"
76
85
  # GitLab
77
86
  # ======
78
87
 
79
- # GitLab settings are similar to those for GitHub.
80
-
81
88
  [gitlab]
82
- # The environment variable is CGITIZE_GITLAB_TOKEN.
89
+ # Personal access token (environment variable CGITIZE_GITLAB_TOKEN).
90
+ # Required scopes (for legacy tokens):
91
+ # * read_api
83
92
  #token = "XXX"
84
- # The environment variable is CGITIZE_GITLAB_USERNAME.
93
+ # Gitlab username (environment variable CGITIZE_GITLAB_USERNAME).
85
94
  #username = "your-username"
86
95
 
87
96
  [gitlab.users.me]
@@ -89,6 +98,9 @@ name = "cgitize-test"
89
98
  dir = "gitlab-dir"
90
99
  #skip = ["repo1", "repo2"]
91
100
 
101
+ public = true
102
+ private = false
103
+
92
104
  # Some random repositories hosted on Gitlab:
93
105
  [gitlab.repositories.gitter]
94
106
  id = "gitterHQ/webapp"
@@ -1,23 +1,23 @@
1
1
  atlassian-python-api==4.0.7
2
- beautifulsoup4==4.14.3
3
- certifi==2026.4.22
2
+ beautifulsoup4==4.15.0
3
+ certifi==2026.5.20
4
4
  cffi==2.0.0
5
5
  charset-normalizer==3.4.7
6
6
  cryptography==48.0.0
7
7
  Deprecated==1.3.1
8
- idna==3.15
8
+ idna==3.18
9
9
  jmespath==1.1.0
10
10
  oauthlib==3.3.1
11
11
  pycparser==3.0
12
12
  PyGithub==2.9.1
13
- PyJWT==2.12.1
13
+ PyJWT==2.13.0
14
14
  PyNaCl==1.6.2
15
15
  python-gitlab==6.5.0
16
16
  requests==2.34.2
17
17
  requests-oauthlib==2.0.0
18
18
  requests-toolbelt==1.0.0
19
- soupsieve==2.8.3
19
+ soupsieve==2.8.4
20
20
  tomli==2.4.1
21
21
  typing_extensions==4.15.0
22
22
  urllib3==2.7.0
23
- wrapt==2.1.2
23
+ wrapt==2.2.1
@@ -25,6 +25,14 @@ clone_via_ssh_false() {
25
25
  sed -i -E -e 's/^clone_via_ssh = (true|false)$/clone_via_ssh = false/' -- "$cgitize_toml_path"
26
26
  }
27
27
 
28
+ private_repos_true() {
29
+ sed -i -E -e 's/^private = (true|false)$/private = true/' -- "$cgitize_toml_path"
30
+ }
31
+
32
+ private_repos_false() {
33
+ sed -i -E -e 's/^private = (true|false)$/private = false/' -- "$cgitize_toml_path"
34
+ }
35
+
28
36
  cleanup() {
29
37
  echo
30
38
  echo ----------------------------------------------------------------------
@@ -35,6 +43,8 @@ cleanup() {
35
43
  rm -rf -- "$output_dir"
36
44
  echo "Reverting clone_via_ssh settings: $cgitize_toml_path"
37
45
  clone_via_ssh_false
46
+ echo "Reverting repository visibility settings: $cgitize_toml_path"
47
+ private_repos_false
38
48
  }
39
49
 
40
50
  cgitize() {
@@ -54,6 +64,10 @@ setup_https() {
54
64
  clone_via_ssh_false
55
65
  }
56
66
 
67
+ setup_private_repos() {
68
+ private_repos_true
69
+ }
70
+
57
71
  verify_origin() {
58
72
  if [ "$#" -ne 2 ]; then
59
73
  echo "usage: ${FUNCNAME[0]} REPO URL" >&2
@@ -129,6 +143,44 @@ test_ssh() {
129
143
  setup_ssh
130
144
  cgitize
131
145
 
146
+ verify_repos \
147
+ lens \
148
+ chromiumembedded/cef \
149
+ graphviz \
150
+ wireguard/wintun \
151
+ goauthentik-dir/authentik \
152
+ github-dir/public \
153
+ bitbucket-dir/public \
154
+ gitlab-dir/public
155
+
156
+ verify_no_repos \
157
+ github-dir/private \
158
+ bitbucket-dir/private \
159
+ gitlab-dir/private
160
+
161
+ verify_origin lens 'git@github.com:ekmett/lens.git'
162
+ verify_origin chromiumembedded/cef 'git@bitbucket.org:chromiumembedded/cef.git'
163
+ verify_origin graphviz 'git@gitlab.com:graphviz/graphviz.git'
164
+ verify_origin goauthentik-dir/authentik 'git@github.com:goauthentik/authentik.git'
165
+
166
+ verify_origin github-dir/public 'git@github.com:cgitize-test/public.git'
167
+ verify_origin bitbucket-dir/public 'git@bitbucket.org:cgitize-test-workspace/public.git'
168
+ verify_origin gitlab-dir/public 'git@gitlab.com:cgitize-test/public.git'
169
+
170
+ cleanup
171
+ success
172
+ }
173
+
174
+ test_ssh_private() {
175
+ echo
176
+ echo ======================================================================
177
+ echo "${FUNCNAME[0]}"
178
+ echo ======================================================================
179
+
180
+ setup_ssh
181
+ setup_private_repos
182
+ cgitize
183
+
132
184
  verify_repos \
133
185
  lens \
134
186
  chromiumembedded/cef \
@@ -167,6 +219,44 @@ test_https() {
167
219
  setup_https
168
220
  cgitize
169
221
 
222
+ verify_repos \
223
+ lens \
224
+ chromiumembedded/cef \
225
+ graphviz \
226
+ wireguard/wintun \
227
+ goauthentik-dir/authentik \
228
+ github-dir/public \
229
+ bitbucket-dir/public \
230
+ gitlab-dir/public
231
+
232
+ verify_no_repos \
233
+ github-dir/private \
234
+ bitbucket-dir/private \
235
+ gitlab-dir/private
236
+
237
+ verify_origin lens 'https://github.com/ekmett/lens.git'
238
+ verify_origin chromiumembedded/cef 'https://bitbucket.org/chromiumembedded/cef.git'
239
+ verify_origin graphviz 'https://gitlab.com/graphviz/graphviz.git'
240
+ verify_origin goauthentik-dir/authentik 'https://github.com/goauthentik/authentik.git'
241
+
242
+ verify_origin github-dir/public 'https://github.com/cgitize-test/public.git'
243
+ verify_origin bitbucket-dir/public 'https://bitbucket.org/cgitize-test-workspace/public.git'
244
+ verify_origin gitlab-dir/public 'https://gitlab.com/cgitize-test/public.git'
245
+
246
+ cleanup
247
+ success
248
+ }
249
+
250
+ test_https_private() {
251
+ echo
252
+ echo ======================================================================
253
+ echo "${FUNCNAME[0]}"
254
+ echo ======================================================================
255
+
256
+ setup_https
257
+ setup_private_repos
258
+ cgitize
259
+
170
260
  verify_repos \
171
261
  lens \
172
262
  chromiumembedded/cef \
@@ -213,7 +303,9 @@ test_one_repo() {
213
303
  main() {
214
304
  trap cleanup EXIT
215
305
  test_ssh
306
+ test_ssh_private
216
307
  test_https
308
+ test_https_private
217
309
  test_one_repo
218
310
  }
219
311
 
@@ -1,3 +1,5 @@
1
+ IFS= read -r -s -p 'Bitbucket email: ' CGITIZE_BITBUCKET_EMAIL
2
+ echo
1
3
  IFS= read -r -s -p 'Bitbucket token: ' CGITIZE_BITBUCKET_TOKEN
2
4
  echo
3
5
  IFS= read -r -s -p 'GitHub token: ' CGITIZE_GITHUB_TOKEN
@@ -5,7 +7,7 @@ echo
5
7
  IFS= read -r -s -p 'GitLab token: ' CGITIZE_GITLAB_TOKEN
6
8
  echo
7
9
 
8
- export CGITIZE_BITBUCKET_USERNAME=cgitize-test
10
+ export CGITIZE_BITBUCKET_EMAIL
9
11
  export CGITIZE_BITBUCKET_TOKEN
10
12
  export CGITIZE_GITHUB_USERNAME=cgitize-test
11
13
  export CGITIZE_GITHUB_TOKEN
@@ -13,7 +13,7 @@ from requests.exceptions import HTTPError
13
13
  class BitbucketTests(unittest.TestCase):
14
14
  def setUp(self):
15
15
  self.bitbucket = Cloud(
16
- username=os.environ.get('CGITIZE_BITBUCKET_USERNAME'),
16
+ username=os.environ.get('CGITIZE_BITBUCKET_EMAIL'),
17
17
  password=os.environ.get('CGITIZE_BITBUCKET_TOKEN'),
18
18
  cloud=True,
19
19
  )
@@ -30,6 +30,8 @@ class BitbucketTests(unittest.TestCase):
30
30
  self.assertEqual(r.data['workspace']['name'], 'Test cgitize workspace')
31
31
  self.assertEqual(r.data['workspace']['slug'], 'cgitize-test-workspace')
32
32
 
33
+ self.assertFalse(r.data['is_private'])
34
+
33
35
  self.assertEqual(
34
36
  r.get_link('html'), 'https://bitbucket.org/cgitize-test-workspace/public'
35
37
  )
@@ -57,10 +59,27 @@ class BitbucketTests(unittest.TestCase):
57
59
  self.assertEqual(r.data['workspace']['name'], 'Test cgitize workspace')
58
60
  self.assertEqual(r.data['workspace']['slug'], 'cgitize-test-workspace')
59
61
 
62
+ self.assertTrue(r.data['is_private'])
63
+
60
64
  self.assertEqual(
61
65
  r.get_link('html'), 'https://bitbucket.org/cgitize-test-workspace/private'
62
66
  )
63
67
 
68
+ clone_urls = [
69
+ link for link in r.data['links']['clone'] if link['name'] == 'https'
70
+ ]
71
+ self.assertEqual(len(clone_urls), 1)
72
+ self.assertEqual(
73
+ clone_urls[0]['href'],
74
+ 'https://cgitize-test@bitbucket.org/cgitize-test-workspace/private.git',
75
+ )
76
+
77
+ ssh_urls = [link for link in r.data['links']['clone'] if link['name'] == 'ssh']
78
+ self.assertEqual(len(ssh_urls), 1)
79
+ self.assertEqual(
80
+ ssh_urls[0]['href'], 'git@bitbucket.org:cgitize-test-workspace/private.git'
81
+ )
82
+
64
83
  def test_user(self):
65
84
  w = self.bitbucket.workspaces.get('cgitize-test-workspace')
66
85
  self.assertEqual(
@@ -23,6 +23,7 @@ class GitHubTests(unittest.TestCase):
23
23
  self.assertEqual(r.description, 'Public test cgitize repository')
24
24
  self.assertEqual(r.owner.name, 'Test cgitize user')
25
25
  self.assertEqual(r.owner.login, 'cgitize-test')
26
+ self.assertEqual(r.visibility, 'public')
26
27
  self.assertEqual(r.html_url, 'https://github.com/cgitize-test/public')
27
28
  self.assertEqual(r.clone_url, 'https://github.com/cgitize-test/public.git')
28
29
  self.assertEqual(r.ssh_url, 'git@github.com:cgitize-test/public.git')
@@ -33,6 +34,7 @@ class GitHubTests(unittest.TestCase):
33
34
  self.assertEqual(r.description, 'Private test cgitize repository')
34
35
  self.assertEqual(r.owner.name, 'Test cgitize user')
35
36
  self.assertEqual(r.owner.login, 'cgitize-test')
37
+ self.assertEqual(r.visibility, 'private')
36
38
  self.assertEqual(r.html_url, 'https://github.com/cgitize-test/private')
37
39
  self.assertEqual(r.clone_url, 'https://github.com/cgitize-test/private.git')
38
40
  self.assertEqual(r.ssh_url, 'git@github.com:cgitize-test/private.git')
@@ -26,6 +26,7 @@ class GitLabTests(unittest.TestCase):
26
26
  self.assertEqual(r.description, 'Public test cgitize repository')
27
27
  self.assertEqual(r.namespace['name'], 'Test cgitize user')
28
28
  self.assertEqual(r.namespace['path'], 'cgitize-test')
29
+ self.assertEqual(r.visibility, 'public')
29
30
  self.assertEqual(r.web_url, 'https://gitlab.com/cgitize-test/public')
30
31
  self.assertEqual(
31
32
  r.http_url_to_repo, 'https://gitlab.com/cgitize-test/public.git'
@@ -38,6 +39,7 @@ class GitLabTests(unittest.TestCase):
38
39
  self.assertEqual(r.description, 'Private test cgitize repository')
39
40
  self.assertEqual(r.namespace['name'], 'Test cgitize user')
40
41
  self.assertEqual(r.namespace['path'], 'cgitize-test')
42
+ self.assertEqual(r.visibility, 'private')
41
43
  self.assertEqual(r.web_url, 'https://gitlab.com/cgitize-test/private')
42
44
  self.assertEqual(
43
45
  r.http_url_to_repo, 'https://gitlab.com/cgitize-test/private.git'
@@ -1 +0,0 @@
1
- 3.10.10
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes