|
1
|
#ifdef FOSSIL_ENABLE_JSON |
|
2
|
/* |
|
3
|
** Copyright (c) 2011 D. Richard Hipp |
|
4
|
** |
|
5
|
** This program is free software; you can redistribute it and/or |
|
6
|
** modify it under the terms of the Simplified BSD License (also |
|
7
|
** known as the "2-Clause License" or "FreeBSD License".) |
|
8
|
** |
|
9
|
** This program is distributed in the hope that it will be useful, |
|
10
|
** but without any warranty; without even the implied warranty of |
|
11
|
** merchantability or fitness for a particular purpose. |
|
12
|
** |
|
13
|
** Author contact information: |
|
14
|
** [email protected] |
|
15
|
** http://www.hwaci.com/drh/ |
|
16
|
** |
|
17
|
*/ |
|
18
|
#include "VERSION.h" |
|
19
|
#include "config.h" |
|
20
|
#include "json_branch.h" |
|
21
|
|
|
22
|
#if INTERFACE |
|
23
|
#include "json_detail.h" |
|
24
|
#endif |
|
25
|
|
|
26
|
|
|
27
|
static cson_value * json_branch_list(void); |
|
28
|
static cson_value * json_branch_create(void); |
|
29
|
/* |
|
30
|
** Mapping of /json/branch/XXX commands/paths to callbacks. |
|
31
|
*/ |
|
32
|
static const JsonPageDef JsonPageDefs_Branch[] = { |
|
33
|
{"create", json_branch_create, 0}, |
|
34
|
{"list", json_branch_list, 0}, |
|
35
|
{"new", json_branch_create, -1/* for compat with non-JSON branch command.*/}, |
|
36
|
/* Last entry MUST have a NULL name. */ |
|
37
|
{NULL,NULL,0} |
|
38
|
}; |
|
39
|
|
|
40
|
/* |
|
41
|
** Implements the /json/branch family of pages/commands. Far from |
|
42
|
** complete. |
|
43
|
** |
|
44
|
*/ |
|
45
|
cson_value * json_page_branch(void){ |
|
46
|
return json_page_dispatch_helper(&JsonPageDefs_Branch[0]); |
|
47
|
} |
|
48
|
|
|
49
|
/* |
|
50
|
** Impl for /json/branch/list |
|
51
|
** |
|
52
|
** |
|
53
|
** CLI mode options: |
|
54
|
** |
|
55
|
** -r|--range X, where X is one of (open,closed,all) |
|
56
|
** (only the first letter is significant, default=open) |
|
57
|
** -a (same as --range a) |
|
58
|
** -c (same as --range c) |
|
59
|
** |
|
60
|
** HTTP mode options: |
|
61
|
** |
|
62
|
** "range" GET/POST.payload parameter. FIXME: currently we also use |
|
63
|
** POST, but really want to restrict this to POST.payload. |
|
64
|
*/ |
|
65
|
static cson_value * json_branch_list(void){ |
|
66
|
cson_value * payV; |
|
67
|
cson_object * pay; |
|
68
|
cson_value * listV; |
|
69
|
cson_array * list; |
|
70
|
char const * range = NULL; |
|
71
|
int branchListFlags = BRL_OPEN_ONLY; |
|
72
|
char * sawConversionError = NULL; |
|
73
|
Stmt q = empty_Stmt; |
|
74
|
if( !g.perm.Read ){ |
|
75
|
json_set_err(FSL_JSON_E_DENIED, |
|
76
|
"Requires 'o' permissions."); |
|
77
|
return NULL; |
|
78
|
} |
|
79
|
payV = cson_value_new_object(); |
|
80
|
pay = cson_value_get_object(payV); |
|
81
|
listV = cson_value_new_array(); |
|
82
|
list = cson_value_get_array(listV); |
|
83
|
if(fossil_has_json()){ |
|
84
|
range = json_getenv_cstr("range"); |
|
85
|
} |
|
86
|
|
|
87
|
range = json_find_option_cstr("range",NULL,"r"); |
|
88
|
if((!range||!*range) && !g.isHTTP){ |
|
89
|
range = find_option("all","a",0); |
|
90
|
if(range && *range){ |
|
91
|
range = "a"; |
|
92
|
}else{ |
|
93
|
range = find_option("closed","c",0); |
|
94
|
if(range&&*range){ |
|
95
|
range = "c"; |
|
96
|
} |
|
97
|
} |
|
98
|
} |
|
99
|
|
|
100
|
if(!range || !*range){ |
|
101
|
range = "o"; |
|
102
|
} |
|
103
|
/* Normalize range values... */ |
|
104
|
switch(*range){ |
|
105
|
case 'c': |
|
106
|
range = "closed"; |
|
107
|
branchListFlags = BRL_CLOSED_ONLY; |
|
108
|
break; |
|
109
|
case 'a': |
|
110
|
range = "all"; |
|
111
|
branchListFlags = BRL_BOTH; |
|
112
|
break; |
|
113
|
default: |
|
114
|
range = "open"; |
|
115
|
branchListFlags = BRL_OPEN_ONLY; |
|
116
|
break; |
|
117
|
}; |
|
118
|
cson_object_set(pay,"range",json_new_string(range)); |
|
119
|
|
|
120
|
if( g.localOpen ){ /* add "current" property (branch name). */ |
|
121
|
int vid = db_lget_int("checkout", 0); |
|
122
|
char const * zCurrent = vid |
|
123
|
? db_text(0, "SELECT value FROM tagxref" |
|
124
|
" WHERE rid=%d AND tagid=%d", |
|
125
|
vid, TAG_BRANCH) |
|
126
|
: 0; |
|
127
|
if(zCurrent){ |
|
128
|
cson_object_set(pay,"current",json_new_string(zCurrent)); |
|
129
|
} |
|
130
|
} |
|
131
|
|
|
132
|
|
|
133
|
branch_prepare_list_query(&q, branchListFlags, 0, 0, 0); /* Allow a user? */ |
|
134
|
cson_object_set(pay,"branches",listV); |
|
135
|
while((SQLITE_ROW==db_step(&q))){ |
|
136
|
cson_value * v = cson_sqlite3_column_to_value(q.pStmt,0); |
|
137
|
if(v){ |
|
138
|
cson_array_append(list,v); |
|
139
|
}else if(!sawConversionError){ |
|
140
|
sawConversionError = mprintf("Column-to-json failed @ %s:%d", |
|
141
|
__FILE__,__LINE__); |
|
142
|
} |
|
143
|
} |
|
144
|
if( sawConversionError ){ |
|
145
|
json_warn(FSL_JSON_W_COL_TO_JSON_FAILED,"%s",sawConversionError); |
|
146
|
free(sawConversionError); |
|
147
|
} |
|
148
|
db_finalize(&q); |
|
149
|
return payV; |
|
150
|
} |
|
151
|
|
|
152
|
/* |
|
153
|
** Parameters for the create-branch operation. |
|
154
|
*/ |
|
155
|
typedef struct BranchCreateOptions{ |
|
156
|
char const * zName; |
|
157
|
char const * zBasis; |
|
158
|
char const * zColor; |
|
159
|
int isPrivate; |
|
160
|
/** |
|
161
|
Might be set to an error string by |
|
162
|
json_branch_new(). |
|
163
|
*/ |
|
164
|
char const * rcErrMsg; |
|
165
|
} BranchCreateOptions; |
|
166
|
|
|
167
|
/* |
|
168
|
** Tries to create a new branch based on the options set in zOpt. If |
|
169
|
** an error is encountered, zOpt->rcErrMsg _might_ be set to a |
|
170
|
** descriptive string and one of the FossilJsonCodes values will be |
|
171
|
** returned. Or fossil_fatal() (or similar) might be called, exiting |
|
172
|
** the app. |
|
173
|
** |
|
174
|
** On success 0 is returned and if zNewRid is not NULL then the rid of |
|
175
|
** the new branch is assigned to it. |
|
176
|
** |
|
177
|
** If zOpt->isPrivate is 0 but the parent branch is private, |
|
178
|
** zOpt->isPrivate will be set to a non-zero value and the new branch |
|
179
|
** will be private. |
|
180
|
*/ |
|
181
|
static int json_branch_new(BranchCreateOptions * zOpt, |
|
182
|
int *zNewRid){ |
|
183
|
/* Mostly copied from branch.c:branch_new(), but refactored a small |
|
184
|
bit to not produce output or interact with the user. The |
|
185
|
down-side to that is that we dropped the gpg-signing. It was |
|
186
|
either that or abort the creation if we couldn't sign. We can't |
|
187
|
sign over HTTP mode, anyway. |
|
188
|
*/ |
|
189
|
char const * zBranch = zOpt->zName; |
|
190
|
char const * zBasis = zOpt->zBasis; |
|
191
|
char const * zColor = zOpt->zColor; |
|
192
|
int rootid; /* RID of the root check-in - what we branch off of */ |
|
193
|
int brid; /* RID of the branch check-in */ |
|
194
|
int i; /* Loop counter */ |
|
195
|
char *zUuid; /* Artifact ID of origin */ |
|
196
|
Stmt q; /* Generic query */ |
|
197
|
char *zDate; /* Date that branch was created */ |
|
198
|
char *zComment; /* Check-in comment for the new branch */ |
|
199
|
Blob branch; /* manifest for the new branch */ |
|
200
|
Manifest *pParent; /* Parsed parent manifest */ |
|
201
|
Blob mcksum; /* Self-checksum on the manifest */ |
|
202
|
|
|
203
|
/* fossil branch new name */ |
|
204
|
if( zBranch==0 || zBranch[0]==0 ){ |
|
205
|
zOpt->rcErrMsg = "Branch name may not be null/empty."; |
|
206
|
return FSL_JSON_E_INVALID_ARGS; |
|
207
|
} |
|
208
|
if( db_exists( |
|
209
|
"SELECT 1 FROM tagxref" |
|
210
|
" WHERE tagtype>0" |
|
211
|
" AND tagid=(SELECT tagid FROM tag WHERE tagname='sym-%q')", |
|
212
|
zBranch)!=0 ){ |
|
213
|
zOpt->rcErrMsg = "Branch already exists."; |
|
214
|
return FSL_JSON_E_RESOURCE_ALREADY_EXISTS; |
|
215
|
} |
|
216
|
|
|
217
|
db_begin_transaction(); |
|
218
|
rootid = name_to_typed_rid(zBasis, "ci"); |
|
219
|
if( rootid==0 ){ |
|
220
|
zOpt->rcErrMsg = "Basis branch not found."; |
|
221
|
return FSL_JSON_E_RESOURCE_NOT_FOUND; |
|
222
|
} |
|
223
|
|
|
224
|
pParent = manifest_get(rootid, CFTYPE_MANIFEST, 0); |
|
225
|
if( pParent==0 ){ |
|
226
|
zOpt->rcErrMsg = "Could not read parent manifest."; |
|
227
|
return FSL_JSON_E_UNKNOWN; |
|
228
|
} |
|
229
|
|
|
230
|
/* Create a manifest for the new branch */ |
|
231
|
blob_zero(&branch); |
|
232
|
if( pParent->zBaseline ){ |
|
233
|
blob_appendf(&branch, "B %s\n", pParent->zBaseline); |
|
234
|
} |
|
235
|
zComment = mprintf("Create new branch named \"%s\" " |
|
236
|
"from \"%s\".", zBranch, zBasis); |
|
237
|
blob_appendf(&branch, "C %F\n", zComment); |
|
238
|
free(zComment); |
|
239
|
zDate = date_in_standard_format("now"); |
|
240
|
blob_appendf(&branch, "D %s\n", zDate); |
|
241
|
free(zDate); |
|
242
|
|
|
243
|
/* Copy all of the content from the parent into the branch */ |
|
244
|
for(i=0; i<pParent->nFile; ++i){ |
|
245
|
blob_appendf(&branch, "F %F", pParent->aFile[i].zName); |
|
246
|
if( pParent->aFile[i].zUuid ){ |
|
247
|
blob_appendf(&branch, " %s", pParent->aFile[i].zUuid); |
|
248
|
if( pParent->aFile[i].zPerm && pParent->aFile[i].zPerm[0] ){ |
|
249
|
blob_appendf(&branch, " %s", pParent->aFile[i].zPerm); |
|
250
|
} |
|
251
|
} |
|
252
|
blob_append(&branch, "\n", 1); |
|
253
|
} |
|
254
|
zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rootid); |
|
255
|
blob_appendf(&branch, "P %s\n", zUuid); |
|
256
|
free(zUuid); |
|
257
|
if( pParent->zRepoCksum ){ |
|
258
|
blob_appendf(&branch, "R %s\n", pParent->zRepoCksum); |
|
259
|
} |
|
260
|
manifest_destroy(pParent); |
|
261
|
|
|
262
|
/* Add the symbolic branch name and the "branch" tag to identify |
|
263
|
** this as a new branch */ |
|
264
|
if( content_is_private(rootid) ) zOpt->isPrivate = 1; |
|
265
|
if( zColor!=0 ){ |
|
266
|
blob_appendf(&branch, "T *bgcolor * %F\n", zColor); |
|
267
|
} |
|
268
|
blob_appendf(&branch, "T *branch * %F\n", zBranch); |
|
269
|
blob_appendf(&branch, "T *sym-%F *\n", zBranch); |
|
270
|
|
|
271
|
/* Cancel all other symbolic tags */ |
|
272
|
db_prepare(&q, |
|
273
|
"SELECT tagname FROM tagxref, tag" |
|
274
|
" WHERE tagxref.rid=%d AND tagxref.tagid=tag.tagid" |
|
275
|
" AND tagtype>0 AND tagname GLOB 'sym-*'" |
|
276
|
" ORDER BY tagname", |
|
277
|
rootid); |
|
278
|
while( db_step(&q)==SQLITE_ROW ){ |
|
279
|
const char *zTag = db_column_text(&q, 0); |
|
280
|
blob_appendf(&branch, "T -%F *\n", zTag); |
|
281
|
} |
|
282
|
db_finalize(&q); |
|
283
|
|
|
284
|
blob_appendf(&branch, "U %F\n", g.zLogin); |
|
285
|
md5sum_blob(&branch, &mcksum); |
|
286
|
blob_appendf(&branch, "Z %b\n", &mcksum); |
|
287
|
|
|
288
|
brid = content_put_ex(&branch, 0, 0, 0, zOpt->isPrivate); |
|
289
|
if( brid==0 ){ |
|
290
|
fossil_panic("Problem committing manifest: %s", g.zErrMsg); |
|
291
|
} |
|
292
|
db_add_unsent(brid); |
|
293
|
if( manifest_crosslink(brid, &branch, MC_PERMIT_HOOKS)==0 ){ |
|
294
|
fossil_panic("%s", g.zErrMsg); |
|
295
|
} |
|
296
|
assert( blob_is_reset(&branch) ); |
|
297
|
content_deltify(rootid, &brid, 1, 0); |
|
298
|
if( zNewRid ){ |
|
299
|
*zNewRid = brid; |
|
300
|
} |
|
301
|
|
|
302
|
/* Commit */ |
|
303
|
db_end_transaction(0); |
|
304
|
|
|
305
|
return 0; |
|
306
|
} |
|
307
|
|
|
308
|
|
|
309
|
/* |
|
310
|
** Impl of /json/branch/create. |
|
311
|
*/ |
|
312
|
static cson_value * json_branch_create(void){ |
|
313
|
cson_value * payV = NULL; |
|
314
|
cson_object * pay = NULL; |
|
315
|
int rc = 0; |
|
316
|
BranchCreateOptions opt; |
|
317
|
char * zUuid = NULL; |
|
318
|
const char *zMainBranch = db_main_branch(); |
|
319
|
int rid = 0; |
|
320
|
if( !g.perm.Write ){ |
|
321
|
json_set_err(FSL_JSON_E_DENIED, |
|
322
|
"Requires 'i' permissions."); |
|
323
|
return NULL; |
|
324
|
} |
|
325
|
memset(&opt,0,sizeof(BranchCreateOptions)); |
|
326
|
if(fossil_has_json()){ |
|
327
|
opt.zName = json_getenv_cstr("name"); |
|
328
|
} |
|
329
|
|
|
330
|
if(!opt.zName){ |
|
331
|
opt.zName = json_command_arg(g.json.dispatchDepth+1); |
|
332
|
} |
|
333
|
|
|
334
|
if(!opt.zName){ |
|
335
|
json_set_err(FSL_JSON_E_MISSING_ARGS, |
|
336
|
"'name' parameter was not specified." ); |
|
337
|
return NULL; |
|
338
|
} |
|
339
|
|
|
340
|
opt.zColor = json_find_option_cstr("bgColor","bgcolor",NULL); |
|
341
|
opt.zBasis = json_find_option_cstr("basis",NULL,NULL); |
|
342
|
if(!opt.zBasis && !g.isHTTP){ |
|
343
|
opt.zBasis = json_command_arg(g.json.dispatchDepth+2); |
|
344
|
} |
|
345
|
if(!opt.zBasis){ |
|
346
|
opt.zBasis = fossil_strdup(zMainBranch); |
|
347
|
} |
|
348
|
opt.isPrivate = json_find_option_bool("private",NULL,NULL,-1); |
|
349
|
if(-1==opt.isPrivate){ |
|
350
|
if(!g.isHTTP){ |
|
351
|
opt.isPrivate = (NULL != find_option("private","",0)); |
|
352
|
}else{ |
|
353
|
opt.isPrivate = 0; |
|
354
|
} |
|
355
|
} |
|
356
|
|
|
357
|
rc = json_branch_new( &opt, &rid ); |
|
358
|
if(rc){ |
|
359
|
json_set_err(rc, "%s", opt.rcErrMsg); |
|
360
|
goto error; |
|
361
|
} |
|
362
|
assert(0 != rid); |
|
363
|
payV = cson_value_new_object(); |
|
364
|
pay = cson_value_get_object(payV); |
|
365
|
|
|
366
|
cson_object_set(pay,"name",json_new_string(opt.zName)); |
|
367
|
cson_object_set(pay,"basis",json_new_string(opt.zBasis)); |
|
368
|
cson_object_set(pay,"rid",json_new_int(rid)); |
|
369
|
zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); |
|
370
|
cson_object_set(pay,"uuid", json_new_string(zUuid)); |
|
371
|
cson_object_set(pay, "isPrivate", cson_value_new_bool(opt.isPrivate)); |
|
372
|
free(zUuid); |
|
373
|
if(opt.zColor){ |
|
374
|
cson_object_set(pay,"bgColor",json_new_string(opt.zColor)); |
|
375
|
} |
|
376
|
|
|
377
|
goto ok; |
|
378
|
error: |
|
379
|
assert( 0 != g.json.resultCode ); |
|
380
|
cson_value_free(payV); |
|
381
|
payV = NULL; |
|
382
|
ok: |
|
383
|
return payV; |
|
384
|
} |
|
385
|
|
|
386
|
#endif /* FOSSIL_ENABLE_JSON */ |
|
387
|
|