phable-cli 0.1.7__tar.gz → 0.1.9__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.
- {phable_cli-0.1.7 → phable_cli-0.1.9}/PKG-INFO +32 -6
- {phable_cli-0.1.7 → phable_cli-0.1.9}/README.md +30 -5
- {phable_cli-0.1.7/phable_cli → phable_cli-0.1.9/phable}/cache.py +13 -6
- phable_cli-0.1.9/phable/cli/__init__.py +0 -0
- phable_cli-0.1.9/phable/cli/assign.py +39 -0
- phable_cli-0.1.9/phable/cli/cache.py +22 -0
- phable_cli-0.1.9/phable/cli/comment.py +26 -0
- phable_cli-0.1.9/phable/cli/config.py +25 -0
- phable_cli-0.1.9/phable/cli/create.py +149 -0
- phable_cli-0.1.9/phable/cli/list.py +81 -0
- phable_cli-0.1.9/phable/cli/main.py +97 -0
- phable_cli-0.1.9/phable/cli/move.py +58 -0
- phable_cli-0.1.9/phable/cli/report.py +62 -0
- phable_cli-0.1.9/phable/cli/show.py +35 -0
- phable_cli-0.1.9/phable/cli/subscribe.py +26 -0
- phable_cli-0.1.9/phable/cli/utils.py +1 -0
- {phable_cli-0.1.7/phable_cli → phable_cli-0.1.9/phable}/config.py +18 -9
- phable_cli-0.1.9/phable/display.py +71 -0
- {phable_cli-0.1.7/phable_cli → phable_cli-0.1.9/phable}/phabricator.py +25 -14
- phable_cli-0.1.9/phable/tests/conftest.py +9 -0
- phable_cli-0.1.9/phable/tests/fixtures/simple_task.json +98 -0
- phable_cli-0.1.9/phable/tests/phabricator_test.py +22 -0
- {phable_cli-0.1.7/phable_cli → phable_cli-0.1.9/phable}/utils.py +10 -0
- {phable_cli-0.1.7 → phable_cli-0.1.9}/pyproject.toml +8 -2
- phable_cli-0.1.7/phable_cli/cli.py +0 -496
- {phable_cli-0.1.7 → phable_cli-0.1.9}/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: phable-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.9
|
|
4
4
|
Summary: Manage Phabricator tasks from the comfort of your terminal
|
|
5
5
|
License: MIT
|
|
6
6
|
Author: Balthazar Rouberol
|
|
@@ -14,6 +14,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.13
|
|
16
16
|
Requires-Dist: click (>=8.1.8,<9.0.0)
|
|
17
|
+
Requires-Dist: pytest (>=8.3.5,<9.0.0)
|
|
17
18
|
Requires-Dist: requests (>=2.32.3,<3.0.0)
|
|
18
19
|
Description-Content-Type: text/markdown
|
|
19
20
|
|
|
@@ -58,20 +59,45 @@ Commands:
|
|
|
58
59
|
|
|
59
60
|
## Setup
|
|
60
61
|
|
|
61
|
-
For `phable` to work, you need to define the
|
|
62
|
+
For `phable` to work, you need to define the follwoing configuration, by running `$EDITOR $(phable config show)`:
|
|
62
63
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
```ini
|
|
65
|
+
[phabricator]
|
|
66
|
+
url = # URL to your phabricator instance. Ex: `url = https://phabricator.wikimedia.org`
|
|
67
|
+
token = # API token. Generate a token from ${PHABRICATOR_URL}/settings/user/${YOUR_USERNAME}/page/apitokens/
|
|
68
|
+
default_project_phid = # id for the Phabricator project to be used by default when creating tasks.
|
|
69
|
+
```
|
|
66
70
|
|
|
67
|
-
|
|
71
|
+
To get `default_project_phid`, define the first 2 configurations, and run the following command, where `T123456` is a task id belonging to your project.
|
|
68
72
|
|
|
69
73
|
```console
|
|
70
74
|
$ phable show T123456 --format=json | jq -r '.attachments.projects.projectPHIDs[]'
|
|
71
75
|
```
|
|
72
76
|
|
|
77
|
+
Note: you can also expose these confriguration through the following environment variables, for backwards compatibility:
|
|
78
|
+
- `PHABRICATOR_URL`
|
|
79
|
+
- `PHABRICATOR_TOKEN`
|
|
80
|
+
- `PHABRICATOR_DEFAULT_PROJECT_PHID`
|
|
81
|
+
|
|
73
82
|
## Tips and tricks
|
|
74
83
|
|
|
84
|
+
### Setting up aliases
|
|
85
|
+
You can define command aliases. For example, instead of typing `phable move --column 'Done' --milestone T123456`, you might want to type `phable done T123456`. To do this, open the phable configuration file, with `$EDITOR $(phable config show)` and define an alias:
|
|
86
|
+
|
|
87
|
+
```ini
|
|
88
|
+
[aliases]
|
|
89
|
+
done = move --column 'Done' --milestone
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
I personally currently have the following aliases:
|
|
93
|
+
```console
|
|
94
|
+
$ phable config aliases list
|
|
95
|
+
done = move --column 'Done' --milestone
|
|
96
|
+
review = move --column 'Needs Review' --milestone
|
|
97
|
+
wip = move --column 'In Progress' --milestone
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Phabricator task IDs as clickable links in iTerm2
|
|
75
101
|
If you're using iTerm2, you can turn the task IDs into clickable links, by going to iTerm2 > Settings > Profiles > Advanced > Smart Selection > Edit > [+]:
|
|
76
102
|
- Title: Phabricator Task Id
|
|
77
103
|
- Action: Open URL
|
|
@@ -39,20 +39,45 @@ Commands:
|
|
|
39
39
|
|
|
40
40
|
## Setup
|
|
41
41
|
|
|
42
|
-
For `phable` to work, you need to define the
|
|
42
|
+
For `phable` to work, you need to define the follwoing configuration, by running `$EDITOR $(phable config show)`:
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
```ini
|
|
45
|
+
[phabricator]
|
|
46
|
+
url = # URL to your phabricator instance. Ex: `url = https://phabricator.wikimedia.org`
|
|
47
|
+
token = # API token. Generate a token from ${PHABRICATOR_URL}/settings/user/${YOUR_USERNAME}/page/apitokens/
|
|
48
|
+
default_project_phid = # id for the Phabricator project to be used by default when creating tasks.
|
|
49
|
+
```
|
|
47
50
|
|
|
48
|
-
|
|
51
|
+
To get `default_project_phid`, define the first 2 configurations, and run the following command, where `T123456` is a task id belonging to your project.
|
|
49
52
|
|
|
50
53
|
```console
|
|
51
54
|
$ phable show T123456 --format=json | jq -r '.attachments.projects.projectPHIDs[]'
|
|
52
55
|
```
|
|
53
56
|
|
|
57
|
+
Note: you can also expose these confriguration through the following environment variables, for backwards compatibility:
|
|
58
|
+
- `PHABRICATOR_URL`
|
|
59
|
+
- `PHABRICATOR_TOKEN`
|
|
60
|
+
- `PHABRICATOR_DEFAULT_PROJECT_PHID`
|
|
61
|
+
|
|
54
62
|
## Tips and tricks
|
|
55
63
|
|
|
64
|
+
### Setting up aliases
|
|
65
|
+
You can define command aliases. For example, instead of typing `phable move --column 'Done' --milestone T123456`, you might want to type `phable done T123456`. To do this, open the phable configuration file, with `$EDITOR $(phable config show)` and define an alias:
|
|
66
|
+
|
|
67
|
+
```ini
|
|
68
|
+
[aliases]
|
|
69
|
+
done = move --column 'Done' --milestone
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
I personally currently have the following aliases:
|
|
73
|
+
```console
|
|
74
|
+
$ phable config aliases list
|
|
75
|
+
done = move --column 'Done' --milestone
|
|
76
|
+
review = move --column 'Needs Review' --milestone
|
|
77
|
+
wip = move --column 'In Progress' --milestone
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Phabricator task IDs as clickable links in iTerm2
|
|
56
81
|
If you're using iTerm2, you can turn the task IDs into clickable links, by going to iTerm2 > Settings > Profiles > Advanced > Smart Selection > Edit > [+]:
|
|
57
82
|
- Title: Phabricator Task Id
|
|
58
83
|
- Action: Open URL
|
|
@@ -2,23 +2,30 @@ import inspect
|
|
|
2
2
|
import json
|
|
3
3
|
import os
|
|
4
4
|
import sys
|
|
5
|
+
import tempfile
|
|
5
6
|
import time
|
|
6
7
|
from datetime import datetime
|
|
7
8
|
from functools import wraps
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
if not os.getenv("GITHUB_ACTIONS"):
|
|
12
|
+
CACHE_HOME_PER_PLATFORM = {
|
|
13
|
+
"darwin": Path.home() / "Library" / "Caches",
|
|
14
|
+
"linux": Path(os.getenv("XDG_CACHE_HOME", f"{Path.home()}/.config")),
|
|
15
|
+
"windows": Path("c:/", "Users", os.getlogin(), "AppData", "Local", "Temp"),
|
|
16
|
+
}
|
|
17
|
+
else:
|
|
18
|
+
CACHE_HOME_PER_PLATFORM = {}
|
|
15
19
|
|
|
16
20
|
|
|
17
21
|
class Cache:
|
|
18
22
|
"""A persistent cache for long-lived data, such as user or project"""
|
|
19
23
|
|
|
20
24
|
def __init__(self):
|
|
21
|
-
|
|
25
|
+
cache_parent_dir = CACHE_HOME_PER_PLATFORM.get(sys.platform) or Path(
|
|
26
|
+
tempfile.mkdtemp()
|
|
27
|
+
)
|
|
28
|
+
self.cache_dir = cache_parent_dir / "phind"
|
|
22
29
|
if not self.cache_dir.exists():
|
|
23
30
|
self.cache_dir.mkdir()
|
|
24
31
|
self.cache_filepath = self.cache_dir / "cache.json"
|
|
File without changes
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from phable.phabricator import PhabricatorClient
|
|
5
|
+
from phable.utils import Task
|
|
6
|
+
from phable.cli.utils import VARIADIC
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.command(name="assign")
|
|
10
|
+
@click.option(
|
|
11
|
+
"--username",
|
|
12
|
+
required=False,
|
|
13
|
+
help="The username to assign the task to. Self-assign the task if not provided.",
|
|
14
|
+
)
|
|
15
|
+
@click.argument("task-ids", type=Task.from_str, nargs=VARIADIC, required=True)
|
|
16
|
+
@click.pass_context
|
|
17
|
+
@click.pass_obj
|
|
18
|
+
def assign_task(
|
|
19
|
+
client: PhabricatorClient,
|
|
20
|
+
ctx: click.Context,
|
|
21
|
+
task_ids: list[int],
|
|
22
|
+
username: Optional[str],
|
|
23
|
+
):
|
|
24
|
+
"""Assign one or multiple task ids to a username
|
|
25
|
+
|
|
26
|
+
\b
|
|
27
|
+
Examples:
|
|
28
|
+
$ phable assign T123456 # self assign task
|
|
29
|
+
$ phable assign T123456 brouberol # asign to username
|
|
30
|
+
|
|
31
|
+
"""
|
|
32
|
+
if not username:
|
|
33
|
+
user = client.current_user()
|
|
34
|
+
else:
|
|
35
|
+
user = client.find_user_by_username(username)
|
|
36
|
+
if not user:
|
|
37
|
+
ctx.fail(f"User {username} was not found")
|
|
38
|
+
for task_id in task_ids:
|
|
39
|
+
client.assign_task_to_user(task_id=task_id, user_phid=user["phid"])
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import atexit
|
|
3
|
+
|
|
4
|
+
from phable.cache import cache
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@click.group(name="cache")
|
|
8
|
+
def _cache():
|
|
9
|
+
"""Manage internal cache"""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@_cache.command(name="show")
|
|
13
|
+
def show_cache():
|
|
14
|
+
"""Display the location of the internal phable cache"""
|
|
15
|
+
click.echo(cache.cache_filepath)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@_cache.command()
|
|
19
|
+
def clear():
|
|
20
|
+
"""Delete the phable internal cache file"""
|
|
21
|
+
cache.cache_filepath.unlink(missing_ok=True)
|
|
22
|
+
atexit.unregister(cache.dump) # avoid re-dumping the in-memory cache back to disk
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from phable.utils import Task, text_from_cli_arg_or_fs_or_editor
|
|
4
|
+
from phable.phabricator import PhabricatorClient
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@click.command(name="comment")
|
|
8
|
+
@click.option(
|
|
9
|
+
"--comment",
|
|
10
|
+
type=str,
|
|
11
|
+
help="Comment text or path to a text file containing the comment body. If not provided, an editor will be opened.",
|
|
12
|
+
)
|
|
13
|
+
@click.argument("task-id", type=Task.from_str)
|
|
14
|
+
@click.pass_obj
|
|
15
|
+
def comment_on_task(client: PhabricatorClient, task_id: int, comment: Optional[str]):
|
|
16
|
+
"""Add a comment to a task
|
|
17
|
+
|
|
18
|
+
\b
|
|
19
|
+
Example:
|
|
20
|
+
$ phable comment T123456 --comment 'hello' # set comment body from the cli itself
|
|
21
|
+
$ phable comment T123456 --comment path/to/comment.txt # set comment body from a text file
|
|
22
|
+
$ phable comment T123456 # set comment body from your own text editor
|
|
23
|
+
|
|
24
|
+
"""
|
|
25
|
+
comment = text_from_cli_arg_or_fs_or_editor(comment)
|
|
26
|
+
client.create_or_edit_task(task_id=task_id, params={"comment": comment})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from phable.config import config
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@click.group(name="config")
|
|
6
|
+
def _config():
|
|
7
|
+
"""Manage phable config"""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@_config.command(name="show")
|
|
11
|
+
def show_config():
|
|
12
|
+
"""Display the location of the phable config"""
|
|
13
|
+
click.echo(config.filepath)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@_config.group
|
|
17
|
+
def aliases():
|
|
18
|
+
"""Manage aliases"""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@aliases.command()
|
|
22
|
+
def list():
|
|
23
|
+
"""List configured aliases"""
|
|
24
|
+
for name, alias in config.data.get("aliases", {}).items():
|
|
25
|
+
click.echo(f"{name} = {alias}")
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import re
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from phable.phabricator import PhabricatorClient
|
|
6
|
+
from phable.utils import text_from_cli_arg_or_fs_or_editor, Task
|
|
7
|
+
from phable.config import config
|
|
8
|
+
from phable.cli.show import show_task
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.command(name="create")
|
|
12
|
+
@click.option("--title", required=True, help="Title of the task")
|
|
13
|
+
@click.option(
|
|
14
|
+
"--description",
|
|
15
|
+
help="Task description or path to a file containing the description body. If not provided, an editor will be opened.",
|
|
16
|
+
)
|
|
17
|
+
@click.option(
|
|
18
|
+
"--template",
|
|
19
|
+
type=Path,
|
|
20
|
+
help=(
|
|
21
|
+
"Task description template file. If provided, the --description flag will be ignored "
|
|
22
|
+
"and an editor will be opened, pre-filled with the template file content"
|
|
23
|
+
),
|
|
24
|
+
)
|
|
25
|
+
@click.option(
|
|
26
|
+
"--priority",
|
|
27
|
+
type=click.Choice(["unbreaknow", "high", "normal", "low", "needs-triage"]),
|
|
28
|
+
help="Priority level of the task",
|
|
29
|
+
default="normal",
|
|
30
|
+
)
|
|
31
|
+
@click.option("--parent-id", type=Task.from_str, help="ID of parent task")
|
|
32
|
+
@click.option("--tags", multiple=True, help="Tags to associate to the task")
|
|
33
|
+
@click.option("--cc", multiple=True, help="Subscribers to associate to the task")
|
|
34
|
+
@click.option("--owner", help="The username of the task owner")
|
|
35
|
+
@click.pass_context
|
|
36
|
+
@click.pass_obj
|
|
37
|
+
def create_task(
|
|
38
|
+
client: PhabricatorClient,
|
|
39
|
+
ctx: click.Context,
|
|
40
|
+
title: str,
|
|
41
|
+
description: Optional[str],
|
|
42
|
+
template: Path,
|
|
43
|
+
priority: str,
|
|
44
|
+
parent_id: Optional[str],
|
|
45
|
+
tags: list[str],
|
|
46
|
+
cc: list[str],
|
|
47
|
+
owner: Optional[str],
|
|
48
|
+
):
|
|
49
|
+
"""Create a new task
|
|
50
|
+
|
|
51
|
+
\b
|
|
52
|
+
Examples:
|
|
53
|
+
\b
|
|
54
|
+
# Create a task with a long description by writing it in your favorite text editor
|
|
55
|
+
$ phable create --title 'A task'
|
|
56
|
+
\b
|
|
57
|
+
# Create a task with a long description by pointing it to a description file
|
|
58
|
+
$ phable create --title 'A task' --description path/to/description.txt
|
|
59
|
+
\b
|
|
60
|
+
# Create a task with associated title, priority and desription
|
|
61
|
+
$ phable create --title 'Do the thing!' --priority high --description 'Address the thing right now'
|
|
62
|
+
\b
|
|
63
|
+
# Create a task with associated description template
|
|
64
|
+
$ phable create --title 'Do the thing!' --template ./template.md
|
|
65
|
+
\b
|
|
66
|
+
# Create a task with a given parent
|
|
67
|
+
$ phable create --title 'A subtask' --description 'Subtask description' --parent-id T123456
|
|
68
|
+
\b
|
|
69
|
+
# Create a task with an associated top-level project tag
|
|
70
|
+
$ phable create --title 'A task' --tags 'Data-Platform-SRE'
|
|
71
|
+
\b
|
|
72
|
+
# Create a task with an associated sub-project tag
|
|
73
|
+
$ phable create --title 'A task' --tags 'Data-Platform-SRE (2025.03.22 - 2025.04.11)
|
|
74
|
+
\b
|
|
75
|
+
# Create a task with an associated owner
|
|
76
|
+
$ phable create --title 'A task' --owner brouberol
|
|
77
|
+
\b
|
|
78
|
+
# Create a task with an associated subscriber
|
|
79
|
+
$ phable create --title 'A task' --cc brouberol
|
|
80
|
+
|
|
81
|
+
"""
|
|
82
|
+
if template:
|
|
83
|
+
if template.exists():
|
|
84
|
+
description = template
|
|
85
|
+
force_editor = True
|
|
86
|
+
else:
|
|
87
|
+
ctx.fail(f"Template file {template} does not exist")
|
|
88
|
+
else:
|
|
89
|
+
force_editor = False
|
|
90
|
+
description = text_from_cli_arg_or_fs_or_editor(
|
|
91
|
+
description, force_editor=force_editor
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
task_params = {
|
|
95
|
+
"title": title,
|
|
96
|
+
"description": description,
|
|
97
|
+
"priority": priority,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
tag_projects_phids = []
|
|
101
|
+
for tag in tags:
|
|
102
|
+
# The tag name can be a simple string, or "parent name (subproject name)"
|
|
103
|
+
# In the case of the latter, we need to fetch details for both projects
|
|
104
|
+
if match := re.match(
|
|
105
|
+
r"(?P<parent>[\w\s\.-]+) \((?P<subproject>[\w\s+\.-]+)\)", tag
|
|
106
|
+
):
|
|
107
|
+
parent_title = match.group("parent").strip()
|
|
108
|
+
if parent_project := client.find_project_by_title(title=parent_title):
|
|
109
|
+
parent_project_phid = parent_project["phid"]
|
|
110
|
+
else:
|
|
111
|
+
ctx.fail(f"Project {parent_project} not found")
|
|
112
|
+
project_title = match.group("subproject").strip()
|
|
113
|
+
if project := client.find_project_by_title(
|
|
114
|
+
title=project_title, parent_phid=parent_project_phid
|
|
115
|
+
):
|
|
116
|
+
tag_projects_phids.append(project["phid"])
|
|
117
|
+
else:
|
|
118
|
+
ctx.fail(f"Project {project_title} not found")
|
|
119
|
+
# Simple project name with no subproject
|
|
120
|
+
elif project := client.find_project_by_title(title=tag):
|
|
121
|
+
tag_projects_phids.append(project["phid"])
|
|
122
|
+
else:
|
|
123
|
+
ctx.fail(f"Project {tag} not found")
|
|
124
|
+
if tag_projects_phids:
|
|
125
|
+
task_params["projects.add"] = tag_projects_phids
|
|
126
|
+
else:
|
|
127
|
+
task_params["projects.add"] = [config.phabricator_default_project_phid]
|
|
128
|
+
|
|
129
|
+
if owner:
|
|
130
|
+
if owner_user := client.find_user_by_username(username=owner):
|
|
131
|
+
task_params["owner"] = owner_user["phid"]
|
|
132
|
+
else:
|
|
133
|
+
ctx.fail(f"User {owner} not found")
|
|
134
|
+
|
|
135
|
+
if parent_id:
|
|
136
|
+
parent = client.show_task(parent_id)
|
|
137
|
+
task_params["parents.set"] = [parent["phid"]]
|
|
138
|
+
|
|
139
|
+
cc_phids = []
|
|
140
|
+
for username in cc:
|
|
141
|
+
if user := client.find_user_by_username(username=username):
|
|
142
|
+
cc_phids.append(user["phid"])
|
|
143
|
+
else:
|
|
144
|
+
ctx.fail(f"User {owner} not found")
|
|
145
|
+
if cc_phids:
|
|
146
|
+
task_params["subscribers.set"] = cc_phids
|
|
147
|
+
|
|
148
|
+
task = client.create_or_edit_task(task_params)
|
|
149
|
+
ctx.invoke(show_task, task_id=task["result"]["object"]["id"])
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from phable.phabricator import PhabricatorClient
|
|
5
|
+
from phable.config import config
|
|
6
|
+
from phable.display import display_tasks
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.command(name="list")
|
|
10
|
+
@click.option(
|
|
11
|
+
"--column",
|
|
12
|
+
"columns",
|
|
13
|
+
required=False,
|
|
14
|
+
help="The columns the tasks should be located in",
|
|
15
|
+
multiple=True,
|
|
16
|
+
)
|
|
17
|
+
@click.option(
|
|
18
|
+
"--owner",
|
|
19
|
+
required=False,
|
|
20
|
+
help="The username the tasks should be assigned to",
|
|
21
|
+
)
|
|
22
|
+
@click.option(
|
|
23
|
+
"--milestone/--no-milestone",
|
|
24
|
+
default=False,
|
|
25
|
+
help=(
|
|
26
|
+
"If --milestone is passed, the task will be moved onto the current project's associated "
|
|
27
|
+
"milestone board, instead of the project board itself"
|
|
28
|
+
),
|
|
29
|
+
)
|
|
30
|
+
@click.option(
|
|
31
|
+
"--format",
|
|
32
|
+
required=False,
|
|
33
|
+
type=click.Choice(("html", "plain", "json", "markdown")),
|
|
34
|
+
default="plain",
|
|
35
|
+
help="The output format of the task list",
|
|
36
|
+
)
|
|
37
|
+
@click.pass_context
|
|
38
|
+
@click.pass_obj
|
|
39
|
+
def list_tasks(
|
|
40
|
+
client: PhabricatorClient,
|
|
41
|
+
ctx: click.Context,
|
|
42
|
+
columns: list[str],
|
|
43
|
+
owner: Optional[str] = None,
|
|
44
|
+
milestone: bool = False,
|
|
45
|
+
format: str = "plain",
|
|
46
|
+
):
|
|
47
|
+
"""Lists and filter tasks
|
|
48
|
+
|
|
49
|
+
\b
|
|
50
|
+
Examples:
|
|
51
|
+
# List all tasks in the default board
|
|
52
|
+
$ phable list
|
|
53
|
+
\b
|
|
54
|
+
# List all tasks in the default board latest milestone
|
|
55
|
+
$ phable list --milestone
|
|
56
|
+
\b
|
|
57
|
+
# List all tasks owner by brouberol in the Done column of the default board latest milestone
|
|
58
|
+
$ phable list --milestone --owner brouberol --column Done
|
|
59
|
+
|
|
60
|
+
"""
|
|
61
|
+
if owner:
|
|
62
|
+
owner_user = client.find_user_by_username(owner)["phid"]
|
|
63
|
+
if not owner_user:
|
|
64
|
+
ctx.fail(f"User {owner} was not found")
|
|
65
|
+
else:
|
|
66
|
+
owner_user = None
|
|
67
|
+
project_phid = client.get_main_project_or_milestone(
|
|
68
|
+
project_phid=config.phabricator_default_project_phid, milestone=milestone
|
|
69
|
+
)
|
|
70
|
+
if columns:
|
|
71
|
+
column_phids = [
|
|
72
|
+
client.find_column_in_project(project_phid=project_phid, column_name=column)
|
|
73
|
+
for column in columns
|
|
74
|
+
]
|
|
75
|
+
else:
|
|
76
|
+
column_phids = None
|
|
77
|
+
tasks = client.find_tasks(
|
|
78
|
+
column_phids=column_phids, owner_phid=owner_user, project_phid=project_phid
|
|
79
|
+
)
|
|
80
|
+
tasks = [client.enrich_task(task) for task in tasks]
|
|
81
|
+
display_tasks(tasks=tasks, format=format, separator="")
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import atexit
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from click import Context
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
from phable.cache import cache
|
|
8
|
+
from phable.config import config
|
|
9
|
+
from phable.phabricator import PhabricatorClient
|
|
10
|
+
from phable.cli.assign import assign_task
|
|
11
|
+
from phable.cli.cache import _cache
|
|
12
|
+
from phable.cli.comment import comment_on_task
|
|
13
|
+
from phable.cli.config import _config
|
|
14
|
+
from phable.cli.create import create_task
|
|
15
|
+
from phable.cli.move import move_task
|
|
16
|
+
from phable.cli.report import report_done_tasks
|
|
17
|
+
from phable.cli.show import show_task
|
|
18
|
+
from phable.cli.subscribe import subscribe_to_task
|
|
19
|
+
from phable.cli.list import list_tasks
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AliasedCommandGroup(click.Group):
|
|
23
|
+
"""Custom CLI group allowing the replaement of aliases commands on the fly
|
|
24
|
+
|
|
25
|
+
For example if we have the following configuraion:
|
|
26
|
+
[aliases]
|
|
27
|
+
done = move --column 'Done' --milestone
|
|
28
|
+
|
|
29
|
+
then calling `phable done T123456` will actually call
|
|
30
|
+
`phable move --column Done --milestone T123456` under the hood.
|
|
31
|
+
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, *args, **kwargs):
|
|
35
|
+
super().__init__(*args, **kwargs)
|
|
36
|
+
self._aliases = config.data.get("aliases", {})
|
|
37
|
+
|
|
38
|
+
def make_context(self, info_name, args, parent=None, **extra):
|
|
39
|
+
# First, let's parse the command and handle aliases
|
|
40
|
+
parsed_args = self.parse_command(info_name, args)
|
|
41
|
+
|
|
42
|
+
# Then create the context with the possibly modified args
|
|
43
|
+
ctx = super().make_context(
|
|
44
|
+
info_name=parsed_args[0], args=parsed_args[1:], parent=parent, **extra
|
|
45
|
+
)
|
|
46
|
+
return ctx
|
|
47
|
+
|
|
48
|
+
def parse_command(self, ctx_name, args):
|
|
49
|
+
"""Parse command line arguments and handle aliases"""
|
|
50
|
+
if not args:
|
|
51
|
+
return [ctx_name]
|
|
52
|
+
|
|
53
|
+
# Check if the first argument is an alias
|
|
54
|
+
if args[0] in self._aliases:
|
|
55
|
+
pattern = self._aliases[args[0]]
|
|
56
|
+
# Split the pattern and combine with remaining args
|
|
57
|
+
pattern_parts = click.parser.split_arg_string(pattern)
|
|
58
|
+
# Replace the alias with the pattern parts
|
|
59
|
+
args = pattern_parts + args[1:]
|
|
60
|
+
|
|
61
|
+
return [ctx_name] + args
|
|
62
|
+
|
|
63
|
+
def get_command(self, ctx, cmd_name):
|
|
64
|
+
"""Override to handle aliases in command lookup"""
|
|
65
|
+
if cmd_name in self._aliases:
|
|
66
|
+
return super().get_command(ctx, self._aliases[cmd_name].split()[0])
|
|
67
|
+
return super().get_command(ctx, cmd_name)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@click.group(cls=AliasedCommandGroup)
|
|
71
|
+
@click.version_option(package_name="phable-cli")
|
|
72
|
+
@click.pass_context
|
|
73
|
+
def cli(ctx: Context):
|
|
74
|
+
"""Manage Phabricator tasks from the comfort of your terminal"""
|
|
75
|
+
ctx.obj = PhabricatorClient(config.phabricator_url, config.phabricator_token)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
cli.add_command(assign_task)
|
|
79
|
+
cli.add_command(_cache)
|
|
80
|
+
cli.add_command(comment_on_task)
|
|
81
|
+
cli.add_command(_config)
|
|
82
|
+
cli.add_command(create_task)
|
|
83
|
+
cli.add_command(move_task)
|
|
84
|
+
cli.add_command(report_done_tasks)
|
|
85
|
+
cli.add_command(show_task)
|
|
86
|
+
cli.add_command(subscribe_to_task)
|
|
87
|
+
cli.add_command(list_tasks)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def runcli():
|
|
91
|
+
# Dump the in-memory cache to disk when existing the CLI
|
|
92
|
+
atexit.register(cache.dump)
|
|
93
|
+
cli(max_content_width=120)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
if __name__ == "__main__":
|
|
97
|
+
runcli()
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from phable.config import config
|
|
4
|
+
from phable.cli.utils import VARIADIC
|
|
5
|
+
from phable.phabricator import PhabricatorClient
|
|
6
|
+
from phable.utils import Task
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.command(name="move")
|
|
10
|
+
@click.option(
|
|
11
|
+
"--column",
|
|
12
|
+
type=str,
|
|
13
|
+
required=True,
|
|
14
|
+
help="Name of destination column on the current project board",
|
|
15
|
+
)
|
|
16
|
+
@click.option(
|
|
17
|
+
"--milestone/--no-milestone",
|
|
18
|
+
default=False,
|
|
19
|
+
help=(
|
|
20
|
+
"If --milestone is passed, the task will be moved onto the current project's associated "
|
|
21
|
+
"milestone board, instead of the project board itself"
|
|
22
|
+
),
|
|
23
|
+
)
|
|
24
|
+
@click.argument("task-ids", type=Task.from_str, nargs=VARIADIC, required=True)
|
|
25
|
+
@click.pass_context
|
|
26
|
+
@click.pass_obj
|
|
27
|
+
def move_task(
|
|
28
|
+
client: PhabricatorClient,
|
|
29
|
+
ctx: click.Context,
|
|
30
|
+
task_ids: list[int],
|
|
31
|
+
column: Optional[str],
|
|
32
|
+
milestone: bool,
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Move one or several task on their current project board
|
|
35
|
+
|
|
36
|
+
If the task is moved to a 'Done' column, it will be automatically
|
|
37
|
+
marked as 'Resolved' as well.
|
|
38
|
+
|
|
39
|
+
\b
|
|
40
|
+
Example:
|
|
41
|
+
$ phable move T123456 --column 'In Progress'
|
|
42
|
+
$ phable move T123456 T234567 --column 'Done'
|
|
43
|
+
|
|
44
|
+
"""
|
|
45
|
+
try:
|
|
46
|
+
target_project_phid = client.get_main_project_or_milestone(
|
|
47
|
+
milestone, config.phabricator_default_project_phid
|
|
48
|
+
)
|
|
49
|
+
target_column_phid = client.find_column_in_project(target_project_phid, column)
|
|
50
|
+
|
|
51
|
+
for task_id in task_ids:
|
|
52
|
+
client.move_task_to_column(task_id=task_id, column_phid=target_column_phid)
|
|
53
|
+
if column.lower() in ("in progress", "needs review"):
|
|
54
|
+
client.mark_task_as_in_progress(task_id)
|
|
55
|
+
if column.lower() == "done":
|
|
56
|
+
client.mark_task_as_resolved(task_id)
|
|
57
|
+
except ValueError as ve:
|
|
58
|
+
ctx.fail(ve)
|