standup-cli-tool 0.2.0__tar.gz → 0.3.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.
- {standup_cli_tool-0.2.0 → standup_cli_tool-0.3.0}/PKG-INFO +15 -5
- {standup_cli_tool-0.2.0 → standup_cli_tool-0.3.0}/README.md +147 -137
- {standup_cli_tool-0.2.0 → standup_cli_tool-0.3.0}/pyproject.toml +1 -1
- {standup_cli_tool-0.2.0 → standup_cli_tool-0.3.0}/standup_cli/main.py +404 -351
- {standup_cli_tool-0.2.0 → standup_cli_tool-0.3.0}/standup_cli_tool.egg-info/PKG-INFO +15 -5
- {standup_cli_tool-0.2.0 → standup_cli_tool-0.3.0}/setup.cfg +0 -0
- {standup_cli_tool-0.2.0 → standup_cli_tool-0.3.0}/standup_cli_tool.egg-info/SOURCES.txt +0 -0
- {standup_cli_tool-0.2.0 → standup_cli_tool-0.3.0}/standup_cli_tool.egg-info/dependency_links.txt +0 -0
- {standup_cli_tool-0.2.0 → standup_cli_tool-0.3.0}/standup_cli_tool.egg-info/entry_points.txt +0 -0
- {standup_cli_tool-0.2.0 → standup_cli_tool-0.3.0}/standup_cli_tool.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: standup-cli-tool
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: ⚡ Generate your daily standup from git commits — right in your terminal
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/muhtalhakhan/standup-cli
|
|
@@ -15,12 +15,14 @@ Classifier: Topic :: Utilities
|
|
|
15
15
|
Requires-Python: >=3.8
|
|
16
16
|
Description-Content-Type: text/markdown
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
# ⚡ standup-cli
|
|
19
19
|
|
|
20
20
|
> Generate your daily standup from git commits — right in your terminal.
|
|
21
21
|
|
|
22
22
|
Never manually write a standup again. `standup-cli` scans your git commits from the last 24 hours, asks what you're working on today and if you have blockers, then formats a clean standup message ready to paste anywhere.
|
|
23
23
|
|
|
24
|
+

|
|
25
|
+
|
|
24
26
|
```bash
|
|
25
27
|
$ standup
|
|
26
28
|
|
|
@@ -85,15 +87,19 @@ standup --no-copy
|
|
|
85
87
|
|
|
86
88
|
# Scan multiple repositories
|
|
87
89
|
standup --repo . --repo ../another-repo
|
|
90
|
+
|
|
91
|
+
# Weekly summary mode
|
|
92
|
+
standup --weekly
|
|
88
93
|
```
|
|
89
94
|
|
|
90
95
|
## What It Includes
|
|
91
96
|
|
|
92
97
|
- Conventional Commit parsing (`feat`, `fix`, `docs`, etc.) into grouped sections
|
|
93
|
-
- Files changed count per repository (last 24h window)
|
|
98
|
+
- Files changed count per repository (last 24h or weekly window)
|
|
94
99
|
- Output grouped by repository
|
|
95
100
|
- Clipboard auto-copy by default
|
|
96
101
|
- `.standuprc` support for defaults
|
|
102
|
+
- Weekly summaries with `--weekly`
|
|
97
103
|
|
|
98
104
|
## .standuprc
|
|
99
105
|
|
|
@@ -106,6 +112,7 @@ JSON format:
|
|
|
106
112
|
"format": "slack",
|
|
107
113
|
"team": "Platform",
|
|
108
114
|
"copy": true,
|
|
115
|
+
"weekly": false,
|
|
109
116
|
"repos": [".", "../service-api"]
|
|
110
117
|
}
|
|
111
118
|
```
|
|
@@ -116,6 +123,7 @@ Key-value format is also supported:
|
|
|
116
123
|
format=plain
|
|
117
124
|
team=Platform
|
|
118
125
|
copy=true
|
|
126
|
+
weekly=false
|
|
119
127
|
repos=.,../service-api
|
|
120
128
|
```
|
|
121
129
|
|
|
@@ -165,14 +173,16 @@ None
|
|
|
165
173
|
2. Prompts you for today's focus and any blockers
|
|
166
174
|
3. Formats and prints your standup
|
|
167
175
|
|
|
176
|
+
Use `--weekly` to scan commits from the last 7 days and label the summary as `Last week` / `Next`.
|
|
177
|
+
|
|
168
178
|
> **Tip:** Run it from your project root for best results. Works with any git repo.
|
|
169
179
|
|
|
170
180
|
## Roadmap (v1 ideas)
|
|
171
181
|
|
|
172
182
|
- [x] Copy to clipboard automatically
|
|
173
|
-
- [
|
|
183
|
+
- [x] Support multiple repos
|
|
174
184
|
- [x] `.standuprc` config file for team name, format preference
|
|
175
|
-
- [
|
|
185
|
+
- [x] Weekly summary mode
|
|
176
186
|
|
|
177
187
|
## License
|
|
178
188
|
|
|
@@ -1,162 +1,172 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
> Generate your daily standup from git commits — right in your terminal.
|
|
4
|
-
|
|
1
|
+
# ⚡ standup-cli
|
|
2
|
+
|
|
3
|
+
> Generate your daily standup from git commits — right in your terminal.
|
|
4
|
+
|
|
5
5
|
Never manually write a standup again. `standup-cli` scans your git commits from the last 24 hours, asks what you're working on today and if you have blockers, then formats a clean standup message ready to paste anywhere.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
$ standup
|
|
9
|
-
|
|
10
|
-
⚡ standup-cli-tool
|
|
11
|
-
Generate your daily standup in seconds
|
|
12
|
-
|
|
13
|
-
🔍 Scanning git commits from last 24hrs...
|
|
14
|
-
✅ Found 3 commit(s):
|
|
15
|
-
|
|
16
|
-
• Fixed auth bug in login flow
|
|
17
|
-
• Updated API documentation
|
|
18
|
-
• Refactor user model
|
|
19
|
-
|
|
20
|
-
🚀 What are you working on today?
|
|
21
|
-
> Integrating Stripe payment API
|
|
22
|
-
|
|
23
|
-
🚧 Any blockers? (press Enter for "None")
|
|
24
|
-
> None
|
|
25
|
-
|
|
26
|
-
──────────────────────────────────────────────────
|
|
27
|
-
✅ Your Standup [plain]
|
|
28
|
-
|
|
29
|
-
Yesterday: Fixed auth bug in login flow, Updated API documentation, Refactor user model
|
|
30
|
-
Today: Integrating Stripe payment API
|
|
31
|
-
Blockers: None
|
|
32
|
-
──────────────────────────────────────────────────
|
|
33
|
-
|
|
34
|
-
💡 Tip: use --format slack | markdown | plain
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
## Install
|
|
38
|
-
|
|
39
|
-
**via npm**:
|
|
40
|
-
|
|
41
|
-
```bash
|
|
42
|
-
npm install -g standup-cli-tool
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
**via pip**:
|
|
46
|
-
|
|
47
|
-
```bash
|
|
48
|
-
pip install standup-cli-tool
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
## Usage
|
|
7
|
+

|
|
52
8
|
|
|
53
9
|
```bash
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
10
|
+
$ standup
|
|
11
|
+
|
|
12
|
+
⚡ standup-cli-tool
|
|
13
|
+
Generate your daily standup in seconds
|
|
14
|
+
|
|
15
|
+
🔍 Scanning git commits from last 24hrs...
|
|
16
|
+
✅ Found 3 commit(s):
|
|
17
|
+
|
|
18
|
+
• Fixed auth bug in login flow
|
|
19
|
+
• Updated API documentation
|
|
20
|
+
• Refactor user model
|
|
21
|
+
|
|
22
|
+
🚀 What are you working on today?
|
|
23
|
+
> Integrating Stripe payment API
|
|
24
|
+
|
|
25
|
+
🚧 Any blockers? (press Enter for "None")
|
|
26
|
+
> None
|
|
27
|
+
|
|
28
|
+
──────────────────────────────────────────────────
|
|
29
|
+
✅ Your Standup [plain]
|
|
30
|
+
|
|
31
|
+
Yesterday: Fixed auth bug in login flow, Updated API documentation, Refactor user model
|
|
32
|
+
Today: Integrating Stripe payment API
|
|
33
|
+
Blockers: None
|
|
34
|
+
──────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
💡 Tip: use --format slack | markdown | plain
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
**via npm**:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm install -g standup-cli-tool
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**via pip**:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install standup-cli-tool
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Usage
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# Default (plain output, current repo, clipboard on)
|
|
57
|
+
standup
|
|
58
|
+
|
|
59
|
+
# Slack-ready output
|
|
60
|
+
standup --format slack
|
|
61
|
+
|
|
62
|
+
# Markdown output
|
|
63
|
+
standup --format markdown
|
|
64
|
+
|
|
65
|
+
# Team label
|
|
66
|
+
standup --team "Platform"
|
|
67
|
+
|
|
68
|
+
# Disable auto-copy
|
|
69
|
+
standup --no-copy
|
|
70
|
+
|
|
69
71
|
# Scan multiple repositories
|
|
70
72
|
standup --repo . --repo ../another-repo
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
## What It Includes
|
|
74
73
|
|
|
74
|
+
# Weekly summary mode
|
|
75
|
+
standup --weekly
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## What It Includes
|
|
79
|
+
|
|
75
80
|
- Conventional Commit parsing (`feat`, `fix`, `docs`, etc.) into grouped sections
|
|
76
|
-
- Files changed count per repository (last 24h window)
|
|
81
|
+
- Files changed count per repository (last 24h or weekly window)
|
|
77
82
|
- Output grouped by repository
|
|
78
83
|
- Clipboard auto-copy by default
|
|
79
84
|
- `.standuprc` support for defaults
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
- Weekly summaries with `--weekly`
|
|
86
|
+
|
|
87
|
+
## .standuprc
|
|
88
|
+
|
|
89
|
+
Place `.standuprc` in the current project or your home directory.
|
|
90
|
+
|
|
91
|
+
JSON format:
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
89
95
|
"format": "slack",
|
|
90
96
|
"team": "Platform",
|
|
91
97
|
"copy": true,
|
|
98
|
+
"weekly": false,
|
|
92
99
|
"repos": [".", "../service-api"]
|
|
93
100
|
}
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
Key-value format is also supported:
|
|
97
|
-
|
|
98
|
-
```ini
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Key-value format is also supported:
|
|
104
|
+
|
|
105
|
+
```ini
|
|
99
106
|
format=plain
|
|
100
107
|
team=Platform
|
|
101
108
|
copy=true
|
|
109
|
+
weekly=false
|
|
102
110
|
repos=.,../service-api
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
## Output Example (plain)
|
|
106
|
-
|
|
107
|
-
```text
|
|
108
|
-
Team: Platform
|
|
109
|
-
Yesterday:
|
|
110
|
-
standup-cli (3 commits, 9 files changed):
|
|
111
|
-
Features:
|
|
112
|
-
- Add repo grouping support
|
|
113
|
-
Fixes:
|
|
114
|
-
- Handle empty commit logs
|
|
115
|
-
|
|
116
|
-
service-api (2 commits, 4 files changed):
|
|
117
|
-
Docs:
|
|
118
|
-
- Update API usage notes
|
|
119
|
-
|
|
120
|
-
Today: Finish release checks
|
|
121
|
-
Blockers: None
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
**Slack** — with bold formatting:
|
|
125
|
-
```
|
|
126
|
-
*📋 Yesterday:* Fixed auth bug, updated docs
|
|
127
|
-
*🚀 Today:* Stripe integration
|
|
128
|
-
*🚧 Blockers:* None
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
**Markdown** — for GitHub, Notion, etc:
|
|
132
|
-
```markdown
|
|
133
|
-
### Daily Standup
|
|
134
|
-
|
|
135
|
-
**Yesterday:**
|
|
136
|
-
Fixed auth bug, updated docs
|
|
137
|
-
|
|
138
|
-
**Today:**
|
|
139
|
-
Stripe integration
|
|
140
|
-
|
|
141
|
-
**Blockers:**
|
|
142
|
-
None
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
## How it works
|
|
146
|
-
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Output Example (plain)
|
|
114
|
+
|
|
115
|
+
```text
|
|
116
|
+
Team: Platform
|
|
117
|
+
Yesterday:
|
|
118
|
+
standup-cli (3 commits, 9 files changed):
|
|
119
|
+
Features:
|
|
120
|
+
- Add repo grouping support
|
|
121
|
+
Fixes:
|
|
122
|
+
- Handle empty commit logs
|
|
123
|
+
|
|
124
|
+
service-api (2 commits, 4 files changed):
|
|
125
|
+
Docs:
|
|
126
|
+
- Update API usage notes
|
|
127
|
+
|
|
128
|
+
Today: Finish release checks
|
|
129
|
+
Blockers: None
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**Slack** — with bold formatting:
|
|
133
|
+
```
|
|
134
|
+
*📋 Yesterday:* Fixed auth bug, updated docs
|
|
135
|
+
*🚀 Today:* Stripe integration
|
|
136
|
+
*🚧 Blockers:* None
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**Markdown** — for GitHub, Notion, etc:
|
|
140
|
+
```markdown
|
|
141
|
+
### Daily Standup
|
|
142
|
+
|
|
143
|
+
**Yesterday:**
|
|
144
|
+
Fixed auth bug, updated docs
|
|
145
|
+
|
|
146
|
+
**Today:**
|
|
147
|
+
Stripe integration
|
|
148
|
+
|
|
149
|
+
**Blockers:**
|
|
150
|
+
None
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## How it works
|
|
154
|
+
|
|
147
155
|
1. Runs `git log --since="24 hours ago"` in your current directory
|
|
148
156
|
2. Prompts you for today's focus and any blockers
|
|
149
157
|
3. Formats and prints your standup
|
|
150
158
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
## Roadmap (v1 ideas)
|
|
159
|
+
Use `--weekly` to scan commits from the last 7 days and label the summary as `Last week` / `Next`.
|
|
154
160
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
161
|
+
> **Tip:** Run it from your project root for best results. Works with any git repo.
|
|
162
|
+
|
|
163
|
+
## Roadmap (v1 ideas)
|
|
164
|
+
|
|
165
|
+
- [x] Copy to clipboard automatically
|
|
166
|
+
- [x] Support multiple repos
|
|
167
|
+
- [x] `.standuprc` config file for team name, format preference
|
|
168
|
+
- [x] Weekly summary mode
|
|
169
|
+
|
|
170
|
+
## License
|
|
171
|
+
|
|
172
|
+
MIT © [Muhammad Talha Khan](https://github.com/muhtalhakhan)
|
|
@@ -1,246 +1,254 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
|
|
3
|
-
import argparse
|
|
4
|
-
import json
|
|
5
|
-
import os
|
|
6
|
-
import re
|
|
7
|
-
import shutil
|
|
8
|
-
import subprocess
|
|
9
|
-
import sys
|
|
10
|
-
|
|
11
|
-
# ANSI colors
|
|
12
|
-
RESET = "\x1b[0m"
|
|
13
|
-
BOLD = "\x1b[1m"
|
|
14
|
-
DIM = "\x1b[2m"
|
|
15
|
-
CYAN = "\x1b[36m"
|
|
16
|
-
GREEN = "\x1b[32m"
|
|
17
|
-
YELLOW = "\x1b[33m"
|
|
18
|
-
MAGENTA = "\x1b[35m"
|
|
19
|
-
GRAY = "\x1b[90m"
|
|
20
|
-
|
|
21
|
-
TYPE_ORDER = [
|
|
22
|
-
"feat",
|
|
23
|
-
"fix",
|
|
24
|
-
"refactor",
|
|
25
|
-
"perf",
|
|
26
|
-
"docs",
|
|
27
|
-
"test",
|
|
28
|
-
"chore",
|
|
29
|
-
"build",
|
|
30
|
-
"ci",
|
|
31
|
-
"style",
|
|
32
|
-
"other",
|
|
33
|
-
]
|
|
34
|
-
|
|
35
|
-
TYPE_LABELS = {
|
|
36
|
-
"feat": "Features",
|
|
37
|
-
"fix": "Fixes",
|
|
38
|
-
"refactor": "Refactors",
|
|
39
|
-
"perf": "Performance",
|
|
40
|
-
"docs": "Docs",
|
|
41
|
-
"test": "Tests",
|
|
42
|
-
"chore": "Chores",
|
|
43
|
-
"build": "Build",
|
|
44
|
-
"ci": "CI",
|
|
45
|
-
"style": "Style",
|
|
46
|
-
"other": "Other",
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def paint(color, text):
|
|
51
|
-
return f"{color}{text}{RESET}"
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def parse_bool(value, default):
|
|
55
|
-
if value is None:
|
|
56
|
-
return default
|
|
57
|
-
if isinstance(value, bool):
|
|
58
|
-
return value
|
|
59
|
-
raw = str(value).strip().lower()
|
|
60
|
-
if raw in ("1", "true", "yes", "on"):
|
|
61
|
-
return True
|
|
62
|
-
if raw in ("0", "false", "no", "off"):
|
|
63
|
-
return False
|
|
64
|
-
return default
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def normalize_repos(value):
|
|
68
|
-
if value is None:
|
|
69
|
-
return []
|
|
70
|
-
if isinstance(value, str):
|
|
71
|
-
return [v.strip() for v in value.split(",") if v.strip()]
|
|
72
|
-
if isinstance(value, list):
|
|
73
|
-
return [str(v).strip() for v in value if str(v).strip()]
|
|
74
|
-
return []
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def load_config():
|
|
78
|
-
paths = [
|
|
79
|
-
os.path.join(os.getcwd(), ".standuprc"),
|
|
80
|
-
os.path.join(os.path.expanduser("~"), ".standuprc"),
|
|
81
|
-
]
|
|
82
|
-
for path in paths:
|
|
83
|
-
if not os.path.isfile(path):
|
|
84
|
-
continue
|
|
85
|
-
try:
|
|
86
|
-
with open(path, "r", encoding="utf-8") as handle:
|
|
87
|
-
raw = handle.read().strip()
|
|
88
|
-
if not raw:
|
|
89
|
-
return {}
|
|
90
|
-
if raw.lstrip().startswith("{"):
|
|
91
|
-
data = json.loads(raw)
|
|
92
|
-
else:
|
|
93
|
-
data = {}
|
|
94
|
-
for line in raw.splitlines():
|
|
95
|
-
line = line.strip()
|
|
96
|
-
if not line or line.startswith("#") or "=" not in line:
|
|
97
|
-
continue
|
|
98
|
-
key, value = line.split("=", 1)
|
|
99
|
-
data[key.strip()] = value.strip()
|
|
100
|
-
config = {}
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
# ANSI colors
|
|
12
|
+
RESET = "\x1b[0m"
|
|
13
|
+
BOLD = "\x1b[1m"
|
|
14
|
+
DIM = "\x1b[2m"
|
|
15
|
+
CYAN = "\x1b[36m"
|
|
16
|
+
GREEN = "\x1b[32m"
|
|
17
|
+
YELLOW = "\x1b[33m"
|
|
18
|
+
MAGENTA = "\x1b[35m"
|
|
19
|
+
GRAY = "\x1b[90m"
|
|
20
|
+
|
|
21
|
+
TYPE_ORDER = [
|
|
22
|
+
"feat",
|
|
23
|
+
"fix",
|
|
24
|
+
"refactor",
|
|
25
|
+
"perf",
|
|
26
|
+
"docs",
|
|
27
|
+
"test",
|
|
28
|
+
"chore",
|
|
29
|
+
"build",
|
|
30
|
+
"ci",
|
|
31
|
+
"style",
|
|
32
|
+
"other",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
TYPE_LABELS = {
|
|
36
|
+
"feat": "Features",
|
|
37
|
+
"fix": "Fixes",
|
|
38
|
+
"refactor": "Refactors",
|
|
39
|
+
"perf": "Performance",
|
|
40
|
+
"docs": "Docs",
|
|
41
|
+
"test": "Tests",
|
|
42
|
+
"chore": "Chores",
|
|
43
|
+
"build": "Build",
|
|
44
|
+
"ci": "CI",
|
|
45
|
+
"style": "Style",
|
|
46
|
+
"other": "Other",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def paint(color, text):
|
|
51
|
+
return f"{color}{text}{RESET}"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def parse_bool(value, default):
|
|
55
|
+
if value is None:
|
|
56
|
+
return default
|
|
57
|
+
if isinstance(value, bool):
|
|
58
|
+
return value
|
|
59
|
+
raw = str(value).strip().lower()
|
|
60
|
+
if raw in ("1", "true", "yes", "on"):
|
|
61
|
+
return True
|
|
62
|
+
if raw in ("0", "false", "no", "off"):
|
|
63
|
+
return False
|
|
64
|
+
return default
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def normalize_repos(value):
|
|
68
|
+
if value is None:
|
|
69
|
+
return []
|
|
70
|
+
if isinstance(value, str):
|
|
71
|
+
return [v.strip() for v in value.split(",") if v.strip()]
|
|
72
|
+
if isinstance(value, list):
|
|
73
|
+
return [str(v).strip() for v in value if str(v).strip()]
|
|
74
|
+
return []
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def load_config():
|
|
78
|
+
paths = [
|
|
79
|
+
os.path.join(os.getcwd(), ".standuprc"),
|
|
80
|
+
os.path.join(os.path.expanduser("~"), ".standuprc"),
|
|
81
|
+
]
|
|
82
|
+
for path in paths:
|
|
83
|
+
if not os.path.isfile(path):
|
|
84
|
+
continue
|
|
85
|
+
try:
|
|
86
|
+
with open(path, "r", encoding="utf-8") as handle:
|
|
87
|
+
raw = handle.read().strip()
|
|
88
|
+
if not raw:
|
|
89
|
+
return {}
|
|
90
|
+
if raw.lstrip().startswith("{"):
|
|
91
|
+
data = json.loads(raw)
|
|
92
|
+
else:
|
|
93
|
+
data = {}
|
|
94
|
+
for line in raw.splitlines():
|
|
95
|
+
line = line.strip()
|
|
96
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
97
|
+
continue
|
|
98
|
+
key, value = line.split("=", 1)
|
|
99
|
+
data[key.strip()] = value.strip()
|
|
100
|
+
config = {}
|
|
101
101
|
config["format"] = data.get("format")
|
|
102
102
|
config["team"] = data.get("team")
|
|
103
103
|
config["copy"] = parse_bool(data.get("copy"), True)
|
|
104
|
+
config["weekly"] = parse_bool(data.get("weekly"), False)
|
|
104
105
|
if "no_copy" in data:
|
|
105
106
|
config["copy"] = not parse_bool(data.get("no_copy"), False)
|
|
106
107
|
config["repos"] = normalize_repos(data.get("repos"))
|
|
107
|
-
return config
|
|
108
|
-
except Exception:
|
|
109
|
-
return {}
|
|
110
|
-
return {}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
def parse_commit_subject(subject):
|
|
114
|
-
subject = subject.strip()
|
|
115
|
-
match = re.match(
|
|
116
|
-
r"^(?P<type>[a-zA-Z]+)(\((?P<scope>[^)]+)\))?(?P<breaking>!)?:\s*(?P<msg>.+)$",
|
|
117
|
-
subject,
|
|
118
|
-
)
|
|
119
|
-
if match:
|
|
120
|
-
ctype = match.group("type").lower()
|
|
121
|
-
scope = match.group("scope")
|
|
122
|
-
msg = match.group("msg").strip()
|
|
123
|
-
else:
|
|
124
|
-
ctype = "other"
|
|
125
|
-
scope = None
|
|
126
|
-
msg = subject
|
|
127
|
-
msg = msg.rstrip(".")
|
|
128
|
-
if msg:
|
|
129
|
-
msg = msg[0].upper() + msg[1:]
|
|
130
|
-
if scope:
|
|
131
|
-
msg = f"{scope}: {msg}"
|
|
132
|
-
return {"type": ctype, "message": msg}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
def parse_git_log_with_numstat(raw):
|
|
136
|
-
commits = []
|
|
137
|
-
current = None
|
|
138
|
-
for line in raw.splitlines():
|
|
139
|
-
if line.startswith("__COMMIT__\x1f"):
|
|
140
|
-
if current is not None:
|
|
141
|
-
commits.append(current)
|
|
142
|
-
subject = line.split("\x1f", 1)[1].strip()
|
|
143
|
-
parsed = parse_commit_subject(subject)
|
|
144
|
-
current = {
|
|
145
|
-
"type": parsed["type"],
|
|
146
|
-
"message": parsed["message"],
|
|
147
|
-
"files_changed": 0,
|
|
148
|
-
}
|
|
149
|
-
continue
|
|
150
|
-
|
|
151
|
-
if not line.strip():
|
|
152
|
-
continue
|
|
153
|
-
|
|
154
|
-
if current is None:
|
|
155
|
-
continue
|
|
156
|
-
|
|
157
|
-
parts = line.split("\t")
|
|
158
|
-
if len(parts) >= 3:
|
|
159
|
-
current["files_changed"] += 1
|
|
160
|
-
|
|
161
|
-
if current is not None:
|
|
162
|
-
commits.append(current)
|
|
163
|
-
|
|
164
|
-
return commits
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
def get_repo_summary(repo_path):
|
|
168
|
-
abs_path = os.path.abspath(repo_path)
|
|
169
|
-
repo_name = os.path.basename(abs_path.rstrip("\\/")) or abs_path
|
|
170
|
-
try:
|
|
171
|
-
result = subprocess.run(
|
|
172
|
-
[
|
|
173
|
-
"git",
|
|
108
|
+
return config
|
|
109
|
+
except Exception:
|
|
110
|
+
return {}
|
|
111
|
+
return {}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def parse_commit_subject(subject):
|
|
115
|
+
subject = subject.strip()
|
|
116
|
+
match = re.match(
|
|
117
|
+
r"^(?P<type>[a-zA-Z]+)(\((?P<scope>[^)]+)\))?(?P<breaking>!)?:\s*(?P<msg>.+)$",
|
|
118
|
+
subject,
|
|
119
|
+
)
|
|
120
|
+
if match:
|
|
121
|
+
ctype = match.group("type").lower()
|
|
122
|
+
scope = match.group("scope")
|
|
123
|
+
msg = match.group("msg").strip()
|
|
124
|
+
else:
|
|
125
|
+
ctype = "other"
|
|
126
|
+
scope = None
|
|
127
|
+
msg = subject
|
|
128
|
+
msg = msg.rstrip(".")
|
|
129
|
+
if msg:
|
|
130
|
+
msg = msg[0].upper() + msg[1:]
|
|
131
|
+
if scope:
|
|
132
|
+
msg = f"{scope}: {msg}"
|
|
133
|
+
return {"type": ctype, "message": msg}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def parse_git_log_with_numstat(raw):
|
|
137
|
+
commits = []
|
|
138
|
+
current = None
|
|
139
|
+
for line in raw.splitlines():
|
|
140
|
+
if line.startswith("__COMMIT__\x1f"):
|
|
141
|
+
if current is not None:
|
|
142
|
+
commits.append(current)
|
|
143
|
+
subject = line.split("\x1f", 1)[1].strip()
|
|
144
|
+
parsed = parse_commit_subject(subject)
|
|
145
|
+
current = {
|
|
146
|
+
"type": parsed["type"],
|
|
147
|
+
"message": parsed["message"],
|
|
148
|
+
"files_changed": 0,
|
|
149
|
+
}
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
if not line.strip():
|
|
153
|
+
continue
|
|
154
|
+
|
|
155
|
+
if current is None:
|
|
156
|
+
continue
|
|
157
|
+
|
|
158
|
+
parts = line.split("\t")
|
|
159
|
+
if len(parts) >= 3:
|
|
160
|
+
current["files_changed"] += 1
|
|
161
|
+
|
|
162
|
+
if current is not None:
|
|
163
|
+
commits.append(current)
|
|
164
|
+
|
|
165
|
+
return commits
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def get_repo_summary(repo_path, since):
|
|
169
|
+
abs_path = os.path.abspath(repo_path)
|
|
170
|
+
repo_name = os.path.basename(abs_path.rstrip("\\/")) or abs_path
|
|
171
|
+
try:
|
|
172
|
+
result = subprocess.run(
|
|
173
|
+
[
|
|
174
|
+
"git",
|
|
174
175
|
"-C",
|
|
175
176
|
abs_path,
|
|
176
177
|
"log",
|
|
177
|
-
"--since=
|
|
178
|
-
"--no-merges",
|
|
179
|
-
"--pretty=format:__COMMIT__%x1f%s",
|
|
180
|
-
"--numstat",
|
|
181
|
-
],
|
|
182
|
-
capture_output=True,
|
|
183
|
-
text=True,
|
|
184
|
-
)
|
|
185
|
-
if result.returncode != 0:
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
178
|
+
f"--since={since}",
|
|
179
|
+
"--no-merges",
|
|
180
|
+
"--pretty=format:__COMMIT__%x1f%s",
|
|
181
|
+
"--numstat",
|
|
182
|
+
],
|
|
183
|
+
capture_output=True,
|
|
184
|
+
text=True,
|
|
185
|
+
)
|
|
186
|
+
if result.returncode != 0:
|
|
187
|
+
stderr = (result.stderr or "") + "\n" + (result.stdout or "")
|
|
188
|
+
if "detected dubious ownership in repository" in stderr.lower():
|
|
189
|
+
return {
|
|
190
|
+
"error": "dubious_ownership",
|
|
191
|
+
"path": abs_path,
|
|
192
|
+
"fix": f'git config --global --add safe.directory "{abs_path}"',
|
|
193
|
+
}
|
|
194
|
+
return None
|
|
195
|
+
commits = parse_git_log_with_numstat(result.stdout.strip())
|
|
196
|
+
return {
|
|
197
|
+
"name": repo_name,
|
|
198
|
+
"path": abs_path,
|
|
199
|
+
"commits": commits,
|
|
200
|
+
"commit_count": len(commits),
|
|
201
|
+
"files_changed": sum(c["files_changed"] for c in commits),
|
|
202
|
+
}
|
|
203
|
+
except FileNotFoundError:
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def summarize_commits(commits, empty_message):
|
|
200
208
|
if not commits:
|
|
201
|
-
return [
|
|
202
|
-
|
|
203
|
-
seen = set()
|
|
204
|
-
grouped = {k: [] for k in TYPE_ORDER}
|
|
205
|
-
|
|
206
|
-
for commit in commits:
|
|
207
|
-
message = commit["message"]
|
|
208
|
-
key = message.lower()
|
|
209
|
-
if key in seen:
|
|
210
|
-
continue
|
|
211
|
-
seen.add(key)
|
|
212
|
-
ctype = commit["type"] if commit["type"] in grouped else "other"
|
|
213
|
-
grouped[ctype].append(message)
|
|
214
|
-
|
|
215
|
-
lines = []
|
|
216
|
-
for ctype in TYPE_ORDER:
|
|
217
|
-
items = grouped[ctype]
|
|
218
|
-
if not items:
|
|
219
|
-
continue
|
|
220
|
-
lines.append(f"{TYPE_LABELS.get(ctype, 'Other')}:")
|
|
221
|
-
for item in items:
|
|
222
|
-
lines.append(f"- {item}")
|
|
223
|
-
return lines
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
def format_output(repo_summaries, today, blockers, fmt, team=None):
|
|
227
|
-
repo_lines = []
|
|
228
|
-
if not repo_summaries:
|
|
229
|
-
repo_lines.append("No repositories scanned")
|
|
230
|
-
for repo in repo_summaries:
|
|
231
|
-
repo_lines.append(
|
|
232
|
-
f"{repo['name']} ({repo['commit_count']} commits, {repo['files_changed']} files changed):"
|
|
233
|
-
)
|
|
234
|
-
repo_lines.extend(summarize_commits(repo["commits"]))
|
|
235
|
-
repo_lines.append("")
|
|
236
|
-
if repo_lines and repo_lines[-1] == "":
|
|
237
|
-
repo_lines.pop()
|
|
238
|
-
|
|
209
|
+
return [empty_message]
|
|
210
|
+
|
|
211
|
+
seen = set()
|
|
212
|
+
grouped = {k: [] for k in TYPE_ORDER}
|
|
213
|
+
|
|
214
|
+
for commit in commits:
|
|
215
|
+
message = commit["message"]
|
|
216
|
+
key = message.lower()
|
|
217
|
+
if key in seen:
|
|
218
|
+
continue
|
|
219
|
+
seen.add(key)
|
|
220
|
+
ctype = commit["type"] if commit["type"] in grouped else "other"
|
|
221
|
+
grouped[ctype].append(message)
|
|
222
|
+
|
|
223
|
+
lines = []
|
|
224
|
+
for ctype in TYPE_ORDER:
|
|
225
|
+
items = grouped[ctype]
|
|
226
|
+
if not items:
|
|
227
|
+
continue
|
|
228
|
+
lines.append(f"{TYPE_LABELS.get(ctype, 'Other')}:")
|
|
229
|
+
for item in items:
|
|
230
|
+
lines.append(f"- {item}")
|
|
231
|
+
return lines
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def format_output(repo_summaries, today, blockers, fmt, mode, team=None):
|
|
235
|
+
repo_lines = []
|
|
236
|
+
if not repo_summaries:
|
|
237
|
+
repo_lines.append("No repositories scanned")
|
|
238
|
+
for repo in repo_summaries:
|
|
239
|
+
repo_lines.append(
|
|
240
|
+
f"{repo['name']} ({repo['commit_count']} commits, {repo['files_changed']} files changed):"
|
|
241
|
+
)
|
|
242
|
+
repo_lines.extend(summarize_commits(repo["commits"], mode["empty_message"]))
|
|
243
|
+
repo_lines.append("")
|
|
244
|
+
if repo_lines and repo_lines[-1] == "":
|
|
245
|
+
repo_lines.pop()
|
|
246
|
+
|
|
239
247
|
if fmt == "slack":
|
|
240
248
|
lines = []
|
|
241
249
|
if team:
|
|
242
250
|
lines.append(f"*Team:* {team}")
|
|
243
|
-
lines.append("*
|
|
251
|
+
lines.append(f"*{mode['history_label']}:*")
|
|
244
252
|
for line in repo_lines:
|
|
245
253
|
if not line:
|
|
246
254
|
lines.append("")
|
|
@@ -248,92 +256,99 @@ def format_output(repo_summaries, today, blockers, fmt, team=None):
|
|
|
248
256
|
lines.append(f"*{line}*")
|
|
249
257
|
else:
|
|
250
258
|
lines.append(f"- {line[2:]}" if line.startswith("- ") else f"- {line}")
|
|
251
|
-
lines.append(f"*
|
|
259
|
+
lines.append(f"*{mode['next_label']}:* {today}")
|
|
252
260
|
lines.append(f"*Blockers:* {blockers or 'None'}")
|
|
253
261
|
return "\n".join(lines)
|
|
254
262
|
|
|
255
263
|
if fmt == "markdown":
|
|
256
|
-
lines = ["###
|
|
264
|
+
lines = [f"### {mode['title']}", ""]
|
|
257
265
|
if team:
|
|
258
266
|
lines.extend(["**Team:**", team, ""])
|
|
259
|
-
lines.append("**
|
|
267
|
+
lines.append(f"**{mode['history_label']}:**")
|
|
260
268
|
for line in repo_lines:
|
|
261
269
|
lines.append(line)
|
|
262
|
-
lines.extend(
|
|
270
|
+
lines.extend(
|
|
271
|
+
["", f"**{mode['next_label']}:**", today, "", "**Blockers:**", blockers or "None"]
|
|
272
|
+
)
|
|
263
273
|
return "\n".join(lines)
|
|
264
274
|
|
|
265
275
|
lines = []
|
|
266
276
|
if team:
|
|
267
277
|
lines.append(f"Team: {team}")
|
|
268
|
-
lines.append("
|
|
278
|
+
lines.append(f"{mode['history_label']}:")
|
|
269
279
|
lines.extend(repo_lines)
|
|
270
|
-
lines.append(f"
|
|
280
|
+
lines.append(f"{mode['next_label']}: {today}")
|
|
271
281
|
lines.append(f"Blockers: {blockers or 'None'}")
|
|
272
282
|
return "\n".join(lines)
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
def copy_to_clipboard(text):
|
|
276
|
-
if not text:
|
|
277
|
-
return False
|
|
278
|
-
try:
|
|
279
|
-
if sys.platform.startswith("win"):
|
|
280
|
-
subprocess.run(["clip"], input=text, text=True, check=True)
|
|
281
|
-
return True
|
|
282
|
-
if sys.platform == "darwin" and shutil.which("pbcopy"):
|
|
283
|
-
subprocess.run(["pbcopy"], input=text, text=True, check=True)
|
|
284
|
-
return True
|
|
285
|
-
if shutil.which("xclip"):
|
|
286
|
-
subprocess.run(
|
|
287
|
-
["xclip", "-selection", "clipboard"],
|
|
288
|
-
input=text,
|
|
289
|
-
text=True,
|
|
290
|
-
check=True,
|
|
291
|
-
)
|
|
292
|
-
return True
|
|
293
|
-
if shutil.which("xsel"):
|
|
294
|
-
subprocess.run(
|
|
295
|
-
["xsel", "--clipboard", "--input"], input=text, text=True, check=True
|
|
296
|
-
)
|
|
297
|
-
return True
|
|
298
|
-
except Exception:
|
|
299
|
-
return False
|
|
300
|
-
return False
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
def collect_repo_paths(config, cli_repos):
|
|
304
|
-
source = cli_repos if cli_repos else config.get("repos") or [os.getcwd()]
|
|
305
|
-
seen = set()
|
|
306
|
-
ordered = []
|
|
307
|
-
for repo in source:
|
|
308
|
-
abs_path = os.path.abspath(repo)
|
|
309
|
-
key = abs_path.lower()
|
|
310
|
-
if key in seen:
|
|
311
|
-
continue
|
|
312
|
-
seen.add(key)
|
|
313
|
-
ordered.append(abs_path)
|
|
314
|
-
return ordered
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
def main():
|
|
318
|
-
config = load_config()
|
|
319
|
-
|
|
320
|
-
parser = argparse.ArgumentParser(
|
|
321
|
-
prog="standup", description="Generate your daily standup from git commits"
|
|
322
|
-
)
|
|
323
|
-
parser.add_argument(
|
|
324
|
-
"--format",
|
|
325
|
-
"-f",
|
|
326
|
-
choices=["plain", "slack", "markdown"],
|
|
327
|
-
default=None,
|
|
328
|
-
help="Output format (default: plain)",
|
|
329
|
-
)
|
|
330
|
-
parser.add_argument("--team", "-t", default=None, help="Team name for standup header")
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def copy_to_clipboard(text):
|
|
286
|
+
if not text:
|
|
287
|
+
return False
|
|
288
|
+
try:
|
|
289
|
+
if sys.platform.startswith("win"):
|
|
290
|
+
subprocess.run(["clip"], input=text, text=True, check=True)
|
|
291
|
+
return True
|
|
292
|
+
if sys.platform == "darwin" and shutil.which("pbcopy"):
|
|
293
|
+
subprocess.run(["pbcopy"], input=text, text=True, check=True)
|
|
294
|
+
return True
|
|
295
|
+
if shutil.which("xclip"):
|
|
296
|
+
subprocess.run(
|
|
297
|
+
["xclip", "-selection", "clipboard"],
|
|
298
|
+
input=text,
|
|
299
|
+
text=True,
|
|
300
|
+
check=True,
|
|
301
|
+
)
|
|
302
|
+
return True
|
|
303
|
+
if shutil.which("xsel"):
|
|
304
|
+
subprocess.run(
|
|
305
|
+
["xsel", "--clipboard", "--input"], input=text, text=True, check=True
|
|
306
|
+
)
|
|
307
|
+
return True
|
|
308
|
+
except Exception:
|
|
309
|
+
return False
|
|
310
|
+
return False
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def collect_repo_paths(config, cli_repos):
|
|
314
|
+
source = cli_repos if cli_repos else config.get("repos") or [os.getcwd()]
|
|
315
|
+
seen = set()
|
|
316
|
+
ordered = []
|
|
317
|
+
for repo in source:
|
|
318
|
+
abs_path = os.path.abspath(repo)
|
|
319
|
+
key = abs_path.lower()
|
|
320
|
+
if key in seen:
|
|
321
|
+
continue
|
|
322
|
+
seen.add(key)
|
|
323
|
+
ordered.append(abs_path)
|
|
324
|
+
return ordered
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def main():
|
|
328
|
+
config = load_config()
|
|
329
|
+
|
|
330
|
+
parser = argparse.ArgumentParser(
|
|
331
|
+
prog="standup", description="Generate your daily standup from git commits"
|
|
332
|
+
)
|
|
333
|
+
parser.add_argument(
|
|
334
|
+
"--format",
|
|
335
|
+
"-f",
|
|
336
|
+
choices=["plain", "slack", "markdown"],
|
|
337
|
+
default=None,
|
|
338
|
+
help="Output format (default: plain)",
|
|
339
|
+
)
|
|
340
|
+
parser.add_argument("--team", "-t", default=None, help="Team name for standup header")
|
|
331
341
|
parser.add_argument(
|
|
332
342
|
"--repo",
|
|
333
343
|
action="append",
|
|
334
344
|
default=[],
|
|
335
345
|
help="Repository path to scan (repeatable). Defaults to cwd or .standuprc repos.",
|
|
336
346
|
)
|
|
347
|
+
parser.add_argument(
|
|
348
|
+
"--weekly",
|
|
349
|
+
action="store_true",
|
|
350
|
+
help="Scan the last 7 days and format a weekly summary",
|
|
351
|
+
)
|
|
337
352
|
parser.add_argument("--no-copy", action="store_true", help="Disable clipboard auto-copy")
|
|
338
353
|
args = parser.parse_args()
|
|
339
354
|
|
|
@@ -341,66 +356,104 @@ def main():
|
|
|
341
356
|
team = args.team or config.get("team")
|
|
342
357
|
copy_enabled = (not args.no_copy) and config.get("copy", True)
|
|
343
358
|
repo_paths = collect_repo_paths(config, args.repo)
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
print()
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
359
|
+
weekly = args.weekly or bool(config.get("weekly"))
|
|
360
|
+
mode = (
|
|
361
|
+
{
|
|
362
|
+
"title": "Weekly Standup",
|
|
363
|
+
"since": "7 days ago",
|
|
364
|
+
"scan_label": "last 7 days",
|
|
365
|
+
"history_label": "Last week",
|
|
366
|
+
"next_label": "Next",
|
|
367
|
+
"prompt": " What are you working on next?\n ",
|
|
368
|
+
"empty_message": "No commits in the last 7 days",
|
|
369
|
+
}
|
|
370
|
+
if weekly
|
|
371
|
+
else {
|
|
372
|
+
"title": "Daily Standup",
|
|
373
|
+
"since": "24 hours ago",
|
|
374
|
+
"scan_label": "last 24hrs",
|
|
375
|
+
"history_label": "Yesterday",
|
|
376
|
+
"next_label": "Today",
|
|
377
|
+
"prompt": " What are you working on today?\n ",
|
|
378
|
+
"empty_message": "No commits in the last 24 hours",
|
|
379
|
+
}
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
print()
|
|
383
|
+
print(paint(BOLD + CYAN, " standup-cli"))
|
|
384
|
+
print(paint(GRAY, " Generate your daily standup in seconds\n"))
|
|
385
|
+
|
|
386
|
+
print(paint(DIM, f" Scanning git commits from {mode['scan_label']}..."))
|
|
387
|
+
repo_summaries = []
|
|
388
|
+
skipped = []
|
|
389
|
+
dubious = []
|
|
390
|
+
for repo_path in repo_paths:
|
|
391
|
+
summary = get_repo_summary(repo_path, mode["since"])
|
|
392
|
+
if summary is None:
|
|
393
|
+
skipped.append(repo_path)
|
|
394
|
+
continue
|
|
395
|
+
if "error" in summary:
|
|
396
|
+
if summary["error"] == "dubious_ownership":
|
|
397
|
+
dubious.append(summary)
|
|
398
|
+
else:
|
|
399
|
+
skipped.append(repo_path)
|
|
400
|
+
continue
|
|
401
|
+
repo_summaries.append(summary)
|
|
402
|
+
print(
|
|
403
|
+
paint(
|
|
404
|
+
GREEN,
|
|
405
|
+
f" {summary['name']}: {summary['commit_count']} commit(s), "
|
|
406
|
+
f"{summary['files_changed']} file(s) changed",
|
|
407
|
+
)
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
for issue in dubious:
|
|
411
|
+
print(
|
|
412
|
+
paint(
|
|
413
|
+
YELLOW,
|
|
414
|
+
f" Warning: Git safe.directory blocked repo {issue['path']}",
|
|
415
|
+
)
|
|
416
|
+
)
|
|
417
|
+
print(paint(YELLOW, f" Run: {issue['fix']}"))
|
|
418
|
+
if skipped:
|
|
419
|
+
for repo_path in skipped:
|
|
420
|
+
print(paint(YELLOW, f" Warning: skipped non-git repo {repo_path}"))
|
|
421
|
+
print()
|
|
422
|
+
|
|
423
|
+
today = input(paint(BOLD, mode["prompt"]) + "> ").strip()
|
|
424
|
+
print()
|
|
425
|
+
blockers = input(paint(BOLD, ' Any blockers? (press Enter for "None")\n ') + "> ").strip()
|
|
426
|
+
print()
|
|
427
|
+
|
|
428
|
+
output = format_output(
|
|
429
|
+
repo_summaries,
|
|
378
430
|
today or "(not specified)",
|
|
379
431
|
blockers,
|
|
380
432
|
fmt,
|
|
433
|
+
mode,
|
|
381
434
|
team=team,
|
|
382
435
|
)
|
|
383
|
-
|
|
384
|
-
divider = paint(GRAY, " " + "-" * 50)
|
|
385
|
-
fmt_label = paint(MAGENTA, f"[{fmt}]")
|
|
386
|
-
|
|
387
|
-
print(divider)
|
|
388
|
-
print(paint(BOLD + GREEN, f" Your Standup {fmt_label}\n"))
|
|
389
|
-
for line in output.split("\n"):
|
|
390
|
-
print(" " + line)
|
|
391
|
-
print()
|
|
392
|
-
print(divider)
|
|
393
|
-
print()
|
|
394
|
-
print(paint(GRAY, " Tip: use --format slack | markdown | plain"))
|
|
395
|
-
print()
|
|
396
|
-
|
|
397
|
-
if copy_enabled:
|
|
398
|
-
if copy_to_clipboard(output):
|
|
399
|
-
print(paint(GREEN, " Copied standup to clipboard"))
|
|
400
|
-
else:
|
|
401
|
-
print(paint(YELLOW, " Warning: clipboard copy unavailable"))
|
|
402
|
-
print()
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
if __name__ == "__main__":
|
|
406
|
-
main()
|
|
436
|
+
|
|
437
|
+
divider = paint(GRAY, " " + "-" * 50)
|
|
438
|
+
fmt_label = paint(MAGENTA, f"[{fmt}]")
|
|
439
|
+
|
|
440
|
+
print(divider)
|
|
441
|
+
print(paint(BOLD + GREEN, f" Your Standup {fmt_label}\n"))
|
|
442
|
+
for line in output.split("\n"):
|
|
443
|
+
print(" " + line)
|
|
444
|
+
print()
|
|
445
|
+
print(divider)
|
|
446
|
+
print()
|
|
447
|
+
print(paint(GRAY, " Tip: use --format slack | markdown | plain"))
|
|
448
|
+
print()
|
|
449
|
+
|
|
450
|
+
if copy_enabled:
|
|
451
|
+
if copy_to_clipboard(output):
|
|
452
|
+
print(paint(GREEN, " Copied standup to clipboard"))
|
|
453
|
+
else:
|
|
454
|
+
print(paint(YELLOW, " Warning: clipboard copy unavailable"))
|
|
455
|
+
print()
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
if __name__ == "__main__":
|
|
459
|
+
main()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: standup-cli-tool
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: ⚡ Generate your daily standup from git commits — right in your terminal
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/muhtalhakhan/standup-cli
|
|
@@ -15,12 +15,14 @@ Classifier: Topic :: Utilities
|
|
|
15
15
|
Requires-Python: >=3.8
|
|
16
16
|
Description-Content-Type: text/markdown
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
# ⚡ standup-cli
|
|
19
19
|
|
|
20
20
|
> Generate your daily standup from git commits — right in your terminal.
|
|
21
21
|
|
|
22
22
|
Never manually write a standup again. `standup-cli` scans your git commits from the last 24 hours, asks what you're working on today and if you have blockers, then formats a clean standup message ready to paste anywhere.
|
|
23
23
|
|
|
24
|
+

|
|
25
|
+
|
|
24
26
|
```bash
|
|
25
27
|
$ standup
|
|
26
28
|
|
|
@@ -85,15 +87,19 @@ standup --no-copy
|
|
|
85
87
|
|
|
86
88
|
# Scan multiple repositories
|
|
87
89
|
standup --repo . --repo ../another-repo
|
|
90
|
+
|
|
91
|
+
# Weekly summary mode
|
|
92
|
+
standup --weekly
|
|
88
93
|
```
|
|
89
94
|
|
|
90
95
|
## What It Includes
|
|
91
96
|
|
|
92
97
|
- Conventional Commit parsing (`feat`, `fix`, `docs`, etc.) into grouped sections
|
|
93
|
-
- Files changed count per repository (last 24h window)
|
|
98
|
+
- Files changed count per repository (last 24h or weekly window)
|
|
94
99
|
- Output grouped by repository
|
|
95
100
|
- Clipboard auto-copy by default
|
|
96
101
|
- `.standuprc` support for defaults
|
|
102
|
+
- Weekly summaries with `--weekly`
|
|
97
103
|
|
|
98
104
|
## .standuprc
|
|
99
105
|
|
|
@@ -106,6 +112,7 @@ JSON format:
|
|
|
106
112
|
"format": "slack",
|
|
107
113
|
"team": "Platform",
|
|
108
114
|
"copy": true,
|
|
115
|
+
"weekly": false,
|
|
109
116
|
"repos": [".", "../service-api"]
|
|
110
117
|
}
|
|
111
118
|
```
|
|
@@ -116,6 +123,7 @@ Key-value format is also supported:
|
|
|
116
123
|
format=plain
|
|
117
124
|
team=Platform
|
|
118
125
|
copy=true
|
|
126
|
+
weekly=false
|
|
119
127
|
repos=.,../service-api
|
|
120
128
|
```
|
|
121
129
|
|
|
@@ -165,14 +173,16 @@ None
|
|
|
165
173
|
2. Prompts you for today's focus and any blockers
|
|
166
174
|
3. Formats and prints your standup
|
|
167
175
|
|
|
176
|
+
Use `--weekly` to scan commits from the last 7 days and label the summary as `Last week` / `Next`.
|
|
177
|
+
|
|
168
178
|
> **Tip:** Run it from your project root for best results. Works with any git repo.
|
|
169
179
|
|
|
170
180
|
## Roadmap (v1 ideas)
|
|
171
181
|
|
|
172
182
|
- [x] Copy to clipboard automatically
|
|
173
|
-
- [
|
|
183
|
+
- [x] Support multiple repos
|
|
174
184
|
- [x] `.standuprc` config file for team name, format preference
|
|
175
|
-
- [
|
|
185
|
+
- [x] Weekly summary mode
|
|
176
186
|
|
|
177
187
|
## License
|
|
178
188
|
|
|
File without changes
|
|
File without changes
|
{standup_cli_tool-0.2.0 → standup_cli_tool-0.3.0}/standup_cli_tool.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{standup_cli_tool-0.2.0 → standup_cli_tool-0.3.0}/standup_cli_tool.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|