Skip to content

Commit fe93490

Browse files
committed
feat: add GitignoreParser and corresponding logic in package process
1 parent 151d7b8 commit fe93490

5 files changed

Lines changed: 315 additions & 5 deletions

File tree

pardo-server/.pardoignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
blob

pardo-server/main.hoshi

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,38 @@ func route_user_info(req: kiana.request.Request, params: hashMap.HashMap<str.Str
319319
)
320320
}
321321

322+
// /v1/user/update
323+
func route_user_update(req: kiana.request.Request, params: hashMap.HashMap<str.Str, str.Str>) : kiana.response.Response {
324+
let [user_id, id_err] = get_current_user_id(req)
325+
if (id_err != null) { return make_error_response("not authenticated") }
326+
let [user, u_err] = data_provider.get_user_by_id(user_id)
327+
if (u_err != null) { return make_error_response("user not found") }
328+
329+
let [body_json, err] = req.json()
330+
if (err != null) { return make_error_response("invalid request: error parsing json") }
331+
let body = dyn_cast<json.JSONDict>(body_json)
332+
333+
let email = user.email
334+
let description = user.description
335+
336+
if (body.contains("email")) {
337+
email = dyn_cast<str.Str>(body["email"])
338+
}
339+
if (body.contains("description")) {
340+
description = dyn_cast<str.Str>(body["description"])
341+
}
342+
343+
if (data_provider.update_user(user.id, description, email, user.permission) != dataProvider.ResultCode.Success) {
344+
return make_error_response("failed to update user profile")
345+
}
346+
347+
return kiana.response.Response(
348+
http.HTTPResponseHeader("HTTP/1.1", 200, "OK"),
349+
json.JSONDict()
350+
.put("status", str.Str("ok")) as json.JSONValue
351+
)
352+
}
353+
322354
// /v1/package/list
323355
func route_package_list(req: kiana.request.Request, params: hashMap.HashMap<str.Str, str.Str>) : kiana.response.Response {
324356
let [packages, err] = data_provider.get_package_list()
@@ -1299,6 +1331,7 @@ func main() : int {
12991331
application.use_route("/v1/user/profile/:id", func route_user_public_profile)
13001332
application.use_route("/v1/user/avatar/upload", func route_user_avatar_upload)
13011333
application.use_route("/v1/user/avatar/:id", func route_user_avatar_get)
1334+
application.use_route("/v1/user/update", func route_user_update)
13021335

13031336

13041337
application.use_route("/index/packages/:name/index", func route_index_package_info)

src/pardo/pm/management.hoshi

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use zip "zip"
1313
use net "net"
1414
use runtime "runtime"
1515
use github "pardo/install/github"
16+
use gitignore "pardo/share/gitignore"
1617

1718
struct BasePackageManager {
1819
root_path: fs.Path,
@@ -211,7 +212,7 @@ struct ProjectLocalPackageManager {
211212
prune(manifest: package.PackageManifest, source: package.PackageSource) : lang.Result<bool, str.Str>,
212213
build(manifest: package.PackageManifest, build_manifest: package.BuildManifest) : lang.Result<bool, str.Str>,
213214
package(manifest: package.PackageManifest, build_manifest: package.BuildManifest) : lang.Result<bool, str.Str>,
214-
recursive_zip(z_file: zip.ZipFile, current_path: fs.Path, root_path: fs.Path, zip_name: str.Str) : bool
215+
recursive_zip(z_file: zip.ZipFile, current_path: fs.Path, root_path: fs.Path, zip_name: str.Str, ignore_parser: gitignore.GitignoreParser) : bool
215216
}
216217

217218
impl ProjectLocalPackageManager {
@@ -418,7 +419,25 @@ impl ProjectLocalPackageManager {
418419
let z_file = z_res.unwrap()
419420

420421
let project_root = fs.Path.current_dir()
421-
this.recursive_zip(z_file, project_root, project_root, zip_name)
422+
423+
let ignore_parser = gitignore.GitignoreParser(vec.Vec<gitignore.GitignoreRule>())
424+
let gitignore_path = project_root / ".pardoignore"
425+
if (gitignore_path.exists()) {
426+
let f_res = fs.open(gitignore_path.to_string(), "r")
427+
if (f_res.is_ok()) {
428+
let f = f_res.unwrap()
429+
let content = f.read(f.size())
430+
f.close()
431+
let parse_res = gitignore.GitignoreParser.parse(str.Str(content.data))
432+
if (parse_res.is_ok()) {
433+
ignore_parser = parse_res.unwrap()
434+
} else {
435+
console.print(str.format("Warning: Failed to parse .pardoignore: {}\n", "Unknown error"))
436+
}
437+
}
438+
}
439+
440+
this.recursive_zip(z_file, project_root, project_root, zip_name, ignore_parser)
422441

423442
z_file.close()
424443

@@ -428,7 +447,7 @@ impl ProjectLocalPackageManager {
428447

429448
return lang.Result<bool, str.Str>.ok(true)
430449
},
431-
recursive_zip(z_file: zip.ZipFile, current_path: fs.Path, root_path: fs.Path, zip_name: str.Str) : bool {
450+
recursive_zip(z_file: zip.ZipFile, current_path: fs.Path, root_path: fs.Path, zip_name: str.Str, ignore_parser: gitignore.GitignoreParser) : bool {
432451
let entries = current_path.listdir()
433452
for (let i = 0; i < entries.length; i += 1) {
434453
let entry = entries[i]
@@ -439,8 +458,9 @@ impl ProjectLocalPackageManager {
439458
continue
440459
}
441460

461+
442462
if (entry.is_directory()) {
443-
this.recursive_zip(z_file, entry, root_path, zip_name)
463+
this.recursive_zip(z_file, entry, root_path, zip_name, ignore_parser)
444464
} elif (entry.is_file()) {
445465
let entry_str = entry.to_string()
446466
let root_str = root_path.to_string()
@@ -455,6 +475,10 @@ impl ProjectLocalPackageManager {
455475
} else {
456476
rel_path = filename
457477
}
478+
479+
if (ignore_parser.can_satisfy(fs.Path(rel_path))) {
480+
continue
481+
}
458482

459483
let f_res = fs.open(entry_str, "rb")
460484
if (f_res.is_ok()) {

src/pardo/pm/registry.hoshi

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,22 @@ impl PardoRegistryRemote {
159159
.put("description", str.Str("A hoshi-lang package published via pardo publish") as json.JSONValue)
160160
.put("repository", manifest.repository as json.JSONValue)
161161
.put("license", manifest.license as json.JSONValue)
162-
.put("readme", str.Str("No readme provided") as json.JSONValue)
162+
.put("license", manifest.license as json.JSONValue)
163+
164+
let readme_str = str.Str("No readme provided")
165+
let readme_path = fs.Path("README.md")
166+
if (readme_path.exists()) {
167+
let [f_readme, err_readme] = fs.open(readme_path.to_string(), "r")
168+
if (err_readme == null) {
169+
f_readme.seek(0, 2)
170+
let f_size = f_readme.tell()
171+
f_readme.seek(0, 0)
172+
let buf = f_readme.read(f_size)
173+
readme_str = str.Str(buf.data)
174+
f_readme.close()
175+
}
176+
}
177+
create_payload.put("readme", readme_str as json.JSONValue)
163178

164179
let create_res = this.process_response(this.send_request_with_body("POST", "/v1/package/add", create_payload as json.JSONValue))
165180
if (create_res.is_err()) { return lang.Result<bool, str.Str>.err("Failed to create package: " + create_res.unwrap_err()) }

src/pardo/share/gitignore.hoshi

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
use str "str"
2+
use vec "vec"
3+
use lang "builtin"
4+
use fs "fs"
5+
6+
enum GitignoreParserError {
7+
None
8+
}
9+
10+
struct GitignoreRule {
11+
pattern: str.Str,
12+
negate: bool,
13+
dir_only: bool,
14+
root_anchored: bool,
15+
constructor(pattern: str.Str, negate: bool, dir_only: bool, root_anchored: bool)
16+
}
17+
18+
impl GitignoreRule {
19+
constructor(pattern: str.Str, negate: bool, dir_only: bool, root_anchored: bool) {
20+
this.pattern = pattern
21+
this.negate = negate
22+
this.dir_only = dir_only
23+
this.root_anchored = root_anchored
24+
}
25+
}
26+
27+
struct GitignoreParser {
28+
rules: vec.Vec<GitignoreRule>,
29+
constructor(rules: vec.Vec<GitignoreRule>),
30+
static parse(rules: str.Str) : lang.Result<GitignoreParser, GitignoreParserError>,
31+
can_satisfy(path: fs.Path) : bool,
32+
static match_glob(pattern: str.Str, path: str.Str) : bool,
33+
static match_recursive(pattern: str.Str, p_idx: int, path: str.Str, s_idx: int) : bool
34+
}
35+
36+
impl GitignoreParser {
37+
constructor(rules: vec.Vec<GitignoreRule>) {
38+
this.rules = rules
39+
},
40+
41+
static parse(rules: str.Str) : lang.Result<GitignoreParser, GitignoreParserError> {
42+
let lines = rules.split("\n")
43+
let parsed_rules = vec.Vec<GitignoreRule>()
44+
45+
for (let i = 0; i < lines.length; i += 1) {
46+
let line = lines[i].trim()
47+
48+
if (line.length == 0 || line.startswith("#")) {
49+
continue
50+
}
51+
52+
let negate = false
53+
if (line.startswith("!")) {
54+
negate = true
55+
line = line.substring(1, line.length)
56+
}
57+
58+
let dir_only = false
59+
if (line.endswith("/")) {
60+
dir_only = true
61+
line = line.substring(0, line.length - 1)
62+
}
63+
64+
let root_anchored = false
65+
if (line.startswith("/")) {
66+
root_anchored = true
67+
line = line.substring(1, line.length)
68+
}
69+
// Also anchored if contains / in middle (not implemented for simplicity unless needed, standard says:
70+
// "If the pattern ends with a slash, it is removed for the purpose of the following description, but it would only find a match with a directory. In other words, foo/ will match a directory foo and paths underneath it, but will not match a regular file or a symbolic link foo (this is consistent with the way how pathspec works in general in git)."
71+
// "If the pattern does not contain a slash /, git treats it as a shell glob pattern and checks for a match against the pathname relative to the location of the .gitignore file (relative to the toplevel of the work tree if not from a .gitignore file)."
72+
73+
if (line.find('/') != -1) {
74+
root_anchored = true
75+
}
76+
77+
parsed_rules.push(GitignoreRule(line, negate, dir_only, root_anchored))
78+
}
79+
80+
return lang.Result<GitignoreParser, GitignoreParserError>.ok(GitignoreParser(parsed_rules))
81+
},
82+
83+
can_satisfy(path: fs.Path) : bool {
84+
let ignored = false
85+
// fs.Path might use backslash on Windows, so we verify
86+
let path_norm = path.to_string().replace("\\", "/")
87+
if (path_norm.startswith("./")) {
88+
path_norm = path_norm.substring(2, path_norm.length)
89+
}
90+
91+
for (let i = 0; i < this.rules.length; i += 1) {
92+
let rule = this.rules[i]
93+
let match = false
94+
95+
if (rule.root_anchored) {
96+
match = GitignoreParser.match_glob(rule.pattern, path_norm)
97+
if (match == false && path_norm.startswith(rule.pattern + "/")) {
98+
match = true
99+
}
100+
101+
} else {
102+
103+
if (GitignoreParser.match_glob(rule.pattern, path_norm)) {
104+
match = true
105+
} else {
106+
// Try to match basename
107+
let last_slash = path_norm.rfind('/')
108+
if (last_slash != -1) {
109+
let basename = path_norm.substring(last_slash + 1, path_norm.length)
110+
if (GitignoreParser.match_glob(rule.pattern, basename)) {
111+
match = true
112+
}
113+
}
114+
}
115+
116+
// Also check directories in path?
117+
// if rule is "bin", and path is "bin/main", it should match.
118+
// checking path parts.
119+
if (match == false) {
120+
let parts = path_norm.split("/")
121+
for(let k = 0; k < parts.length; k += 1) {
122+
if (GitignoreParser.match_glob(rule.pattern, parts[k])) {
123+
match = true
124+
break
125+
}
126+
}
127+
}
128+
}
129+
130+
if (match) {
131+
if (rule.negate) {
132+
ignored = false
133+
} else {
134+
ignored = true
135+
}
136+
}
137+
}
138+
return ignored
139+
},
140+
static match_glob(pattern: str.Str, path: str.Str) : bool {
141+
return GitignoreParser.match_recursive(pattern, 0, path, 0)
142+
},
143+
static match_recursive(pattern: str.Str, p_idx: int, path: str.Str, s_idx: int) : bool {
144+
let p_len = pattern.length
145+
let s_len = path.length
146+
147+
while (p_idx < p_len && s_idx < s_len) {
148+
if (pattern[p_idx] == '*') {
149+
// Check for double star
150+
if (p_idx + 1 < p_len && pattern[p_idx + 1] == '*') {
151+
// ** matches zero or more characters including /
152+
p_idx += 2
153+
// Skip following slash if present (**/foo matches foo)
154+
if (p_idx < p_len && pattern[p_idx] == '/') {
155+
p_idx += 1
156+
// If pattern ends with ** or **/, it matches everything
157+
if (p_idx == p_len) { return true }
158+
}
159+
160+
if (p_idx == p_len) { return true } // Trailing ** matches everything
161+
162+
// Try to find next part of pattern in string
163+
// This is brute force recursive.
164+
// Try matching remainder of pattern at every position of string
165+
for (let i = s_idx; i <= s_len; i += 1) {
166+
if (GitignoreParser.match_recursive(pattern, p_idx, path, i)) {
167+
return true
168+
}
169+
}
170+
return false
171+
} else {
172+
// * matches zero or more characters EXCEPT /
173+
p_idx += 1
174+
// Try matching * with empty string
175+
if (GitignoreParser.match_recursive(pattern, p_idx, path, s_idx)) { return true }
176+
177+
// Match * with 1+ chars, but stop at /
178+
for (let i = s_idx; i < s_len; i += 1) {
179+
if (path[i] == '/') { break }
180+
if (GitignoreParser.match_recursive(pattern, p_idx, path, i + 1)) { return true }
181+
}
182+
return false
183+
}
184+
} elif (pattern[p_idx] == '?') {
185+
// ? matches exactly one char except /
186+
if (path[s_idx] == '/') { return false }
187+
p_idx += 1
188+
s_idx += 1
189+
} elif (pattern[p_idx] == '[') {
190+
// Simple character class [abc] or [a-z] can be complex, skipping full implementation for brevity
191+
// unless required. Assuming strict literal or simple matching.
192+
// Let's implement partial support: find ']'
193+
let closing = pattern.find(']', p_idx)
194+
if (closing != -1) {
195+
let match = false
196+
let char_to_check = path[s_idx]
197+
// Iterate content of []
198+
for(let k = p_idx + 1; k < closing; k+=1) {
199+
if (pattern[k] == char_to_check) {
200+
match = true
201+
break
202+
}
203+
// Range support a-z?
204+
if (k + 2 < closing && pattern[k+1] == '-') {
205+
if (char_to_check >= pattern[k] && char_to_check <= pattern[k+2]) {
206+
match = true
207+
break
208+
}
209+
k += 2
210+
}
211+
}
212+
if (match == false) { return false }
213+
p_idx = closing + 1
214+
s_idx += 1
215+
} else {
216+
// No closing ], treat as literal [
217+
if (pattern[p_idx] != path[s_idx]) { return false }
218+
p_idx += 1
219+
s_idx += 1
220+
}
221+
} else {
222+
if (pattern[p_idx] != path[s_idx]) {
223+
return false
224+
}
225+
p_idx += 1
226+
s_idx += 1
227+
}
228+
}
229+
230+
// Handle trailing * in pattern
231+
while (p_idx < p_len && pattern[p_idx] == '*') {
232+
p_idx += 1
233+
}
234+
235+
return p_idx == p_len && s_idx == s_len
236+
}
237+
}

0 commit comments

Comments
 (0)