Add org and repo transfer commands
New commands, one per Forgejo API call used to move the heavy-duty
repositories into the new heavy-duty organization:
- stoke org create (POST /api/v1/orgs)
- stoke org repos (GET /api/v1/orgs/{org}/repos)
- stoke org avatar (POST /api/v1/orgs/{org}/avatar)
- stoke repo transfer (POST /api/v1/repos/{owner}/{repo}/transfer)
All commands documented in the README.
This commit is contained in:
parent
54f80bbbcf
commit
dadfca6093
3 changed files with 206 additions and 1 deletions
78
README.md
78
README.md
|
|
@ -259,6 +259,25 @@ stoke repo import-batch -f repos.json --dry-run
|
||||||
|
|
||||||
Calls `POST /api/v1/repos/migrate` once per entry.
|
Calls `POST /api/v1/repos/migrate` once per entry.
|
||||||
|
|
||||||
|
### `stoke repo transfer`
|
||||||
|
|
||||||
|
Transfer a repository to a new owner (a user or an organization). The authenticated user must have admin rights on the repository and permission to create repositories under the new owner (e.g. be an organization owner), in which case the transfer completes immediately.
|
||||||
|
|
||||||
|
```text
|
||||||
|
Options:
|
||||||
|
-o, --owner <owner> current repository owner (required)
|
||||||
|
-r, --repo <repo> repository name (required)
|
||||||
|
--to <new-owner> new owner: username or organization name (required)
|
||||||
|
```
|
||||||
|
|
||||||
|
Example used to move the heavy-duty repositories into the `heavy-duty` organization:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
stoke repo transfer -o kimi-reviewer-andresmgsl -r box --to heavy-duty
|
||||||
|
```
|
||||||
|
|
||||||
|
Calls `POST /api/v1/repos/{owner}/{repo}/transfer`.
|
||||||
|
|
||||||
### `stoke issue list`
|
### `stoke issue list`
|
||||||
|
|
||||||
List issues in a repository.
|
List issues in a repository.
|
||||||
|
|
@ -334,6 +353,63 @@ stoke collaborator add -o kimi-reviewer-andresmgsl -r infra -u dan --permission
|
||||||
|
|
||||||
Calls `PUT /api/v1/repos/{owner}/{repo}/collaborators/{user}`.
|
Calls `PUT /api/v1/repos/{owner}/{repo}/collaborators/{user}`.
|
||||||
|
|
||||||
|
### `stoke org create`
|
||||||
|
|
||||||
|
Create a new organization. The authenticated user becomes its first owner.
|
||||||
|
|
||||||
|
```text
|
||||||
|
Options:
|
||||||
|
--name <name> organization username, used in URLs (required)
|
||||||
|
--full-name <full-name> display name of the organization
|
||||||
|
-d, --description <description> organization description
|
||||||
|
--website <website> organization website
|
||||||
|
--location <location> organization location
|
||||||
|
--visibility <visibility> public, limited, private (default: public)
|
||||||
|
```
|
||||||
|
|
||||||
|
Example used to create the Heavy Duty Builders organization:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
stoke org create --name heavy-duty --full-name "Heavy Duty Builders" \
|
||||||
|
--website https://heavyduty.builders --visibility public
|
||||||
|
```
|
||||||
|
|
||||||
|
Calls `POST /api/v1/orgs`.
|
||||||
|
|
||||||
|
### `stoke org repos`
|
||||||
|
|
||||||
|
List repositories owned by an organization.
|
||||||
|
|
||||||
|
```text
|
||||||
|
Options:
|
||||||
|
-o, --org <org> organization name (required)
|
||||||
|
-l, --limit <number> maximum repositories to display (default: 50; use 0 for all)
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
stoke org repos -o heavy-duty -l 0
|
||||||
|
```
|
||||||
|
|
||||||
|
Calls `GET /api/v1/orgs/{org}/repos` and auto-paginates.
|
||||||
|
|
||||||
|
### `stoke org avatar`
|
||||||
|
|
||||||
|
Set the avatar (logo) of an organization from a local image file. The image is read, base64-encoded and uploaded; the caller must be an owner of the organization.
|
||||||
|
|
||||||
|
```text
|
||||||
|
Options:
|
||||||
|
-o, --org <org> organization name (required)
|
||||||
|
-f, --file <path> path to the image file (png, jpeg, gif, ...) (required)
|
||||||
|
```
|
||||||
|
|
||||||
|
Example used to set the Heavy Duty Builders logo (sourced from the official GitHub organization avatar at `https://github.com/heavy-duty.png`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
stoke org avatar -o heavy-duty -f heavy-duty-logo.png
|
||||||
|
```
|
||||||
|
|
||||||
|
Calls `POST /api/v1/orgs/{org}/avatar`.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
|
@ -356,7 +432,7 @@ src/
|
||||||
|
|
||||||
## Verification: heavy-duty repository imports
|
## Verification: heavy-duty repository imports
|
||||||
|
|
||||||
The heavy-duty repositories were imported into Forgejo under `https://forgejo.heavyduty.builders/kimi-reviewer-andresmgsl`.
|
The heavy-duty repositories were imported into Forgejo under `https://forgejo.heavyduty.builders/kimi-reviewer-andresmgsl` and later transferred to the `heavy-duty` organization (`https://forgejo.heavyduty.builders/heavy-duty`) using `stoke repo transfer`.
|
||||||
|
|
||||||
| Repository | Visibility | Branches | Commits | Open issues | Total issues | PRs | Labels | Milestones | Releases |
|
| Repository | Visibility | Branches | Commits | Open issues | Total issues | PRs | Labels | Milestones | Releases |
|
||||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||||
|
|
|
||||||
20
src/api.js
20
src/api.js
|
|
@ -155,6 +155,26 @@ class ForgejoClient {
|
||||||
name: newName,
|
name: newName,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async transferRepo(owner, repo, newOwner) {
|
||||||
|
return this.post(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/transfer`, {
|
||||||
|
new_owner: newOwner,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async createOrg(payload) {
|
||||||
|
return this.post('/orgs', payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
async listOrgRepos(org, opts = {}) {
|
||||||
|
return this.getAll(`/orgs/${encodeURIComponent(org)}/repos`, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateOrgAvatar(org, base64Image) {
|
||||||
|
return this.post(`/orgs/${encodeURIComponent(org)}/avatar`, {
|
||||||
|
image: base64Image,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { ForgejoClient };
|
module.exports = { ForgejoClient };
|
||||||
|
|
|
||||||
109
src/cli.js
109
src/cli.js
|
|
@ -442,6 +442,26 @@ repo
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
repo
|
||||||
|
.command('transfer')
|
||||||
|
.description('Transfer a repository to a new owner (user or organization)')
|
||||||
|
.requiredOption('-o, --owner <owner>', 'current repository owner')
|
||||||
|
.requiredOption('-r, --repo <repo>', 'repository name')
|
||||||
|
.requiredOption('--to <new-owner>', 'new owner (username or organization name)')
|
||||||
|
.action(async (options) => {
|
||||||
|
try {
|
||||||
|
const config = loadConfig();
|
||||||
|
const client = ForgejoClient.fromConfig(config);
|
||||||
|
const result = await client.transferRepo(options.owner, options.repo, options.to);
|
||||||
|
console.log(`Repository transferred: ${result.full_name}`);
|
||||||
|
console.log(`URL: ${result.html_url}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Repository transfer failed: ${err.message}`);
|
||||||
|
if (err.status) console.error(`HTTP status: ${err.status}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const issue = program
|
const issue = program
|
||||||
.command('issue')
|
.command('issue')
|
||||||
.description('Manage issues');
|
.description('Manage issues');
|
||||||
|
|
@ -573,6 +593,95 @@ collaborator
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const org = program
|
||||||
|
.command('org')
|
||||||
|
.description('Manage organizations');
|
||||||
|
|
||||||
|
org
|
||||||
|
.command('create')
|
||||||
|
.description('Create a new organization')
|
||||||
|
.requiredOption('--name <name>', 'organization username (short name used in URLs)')
|
||||||
|
.option('--full-name <full-name>', 'display name of the organization', '')
|
||||||
|
.option('-d, --description <description>', 'organization description', '')
|
||||||
|
.option('--website <website>', 'organization website', '')
|
||||||
|
.option('--location <location>', 'organization location', '')
|
||||||
|
.option('--visibility <visibility>', 'visibility: public, limited, private', 'public')
|
||||||
|
.action(async (options) => {
|
||||||
|
try {
|
||||||
|
const config = loadConfig();
|
||||||
|
const client = ForgejoClient.fromConfig(config);
|
||||||
|
const payload = {
|
||||||
|
username: options.name,
|
||||||
|
full_name: options.fullName,
|
||||||
|
description: options.description,
|
||||||
|
website: options.website,
|
||||||
|
location: options.location,
|
||||||
|
visibility: options.visibility,
|
||||||
|
};
|
||||||
|
const result = await client.createOrg(payload);
|
||||||
|
console.log(`Organization created: ${result.username}`);
|
||||||
|
if (result.full_name) console.log(`Full name: ${result.full_name}`);
|
||||||
|
console.log(`URL: ${config.url}/${result.username}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Organization creation failed: ${err.message}`);
|
||||||
|
if (err.status) console.error(`HTTP status: ${err.status}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
org
|
||||||
|
.command('repos')
|
||||||
|
.description('List repositories owned by an organization')
|
||||||
|
.requiredOption('-o, --org <org>', 'organization name')
|
||||||
|
.option('-l, --limit <number>', 'maximum repositories to return', '50')
|
||||||
|
.action(async (options) => {
|
||||||
|
try {
|
||||||
|
const config = loadConfig();
|
||||||
|
const client = ForgejoClient.fromConfig(config);
|
||||||
|
const repos = await client.listOrgRepos(options.org);
|
||||||
|
const limit = Number(options.limit);
|
||||||
|
const display = limit > 0 ? repos.slice(0, limit) : repos;
|
||||||
|
if (!display.length) {
|
||||||
|
console.log('No repositories found.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const r of display) {
|
||||||
|
const vis = r.private ? 'private' : 'public';
|
||||||
|
console.log(`${r.full_name} [${vis}] ${r.html_url}`);
|
||||||
|
}
|
||||||
|
if (repos.length > display.length) {
|
||||||
|
console.log(`...and ${repos.length - display.length} more (use -l 0 for all).`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Failed to list organization repositories: ${err.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
org
|
||||||
|
.command('avatar')
|
||||||
|
.description('Set the avatar (logo) of an organization from an image file')
|
||||||
|
.requiredOption('-o, --org <org>', 'organization name')
|
||||||
|
.requiredOption('-f, --file <path>', 'path to the image file (png, jpeg, gif, ...)')
|
||||||
|
.action(async (options) => {
|
||||||
|
try {
|
||||||
|
const config = loadConfig();
|
||||||
|
const client = ForgejoClient.fromConfig(config);
|
||||||
|
let image;
|
||||||
|
try {
|
||||||
|
image = fs.readFileSync(options.file);
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`Could not read image file ${options.file}: ${err.message}`);
|
||||||
|
}
|
||||||
|
await client.updateOrgAvatar(options.org, image.toString('base64'));
|
||||||
|
console.log(`Avatar updated for organization ${options.org}.`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Failed to update organization avatar: ${err.message}`);
|
||||||
|
if (err.status) console.error(`HTTP status: ${err.status}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
program.parseAsync(process.argv).catch((err) => {
|
program.parseAsync(process.argv).catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue