|
1
|
/* |
|
2
|
** Copyright (c) 2020 D. Richard Hipp |
|
3
|
** |
|
4
|
** This program is free software; you can redistribute it and/or |
|
5
|
** modify it under the terms of the Simplified BSD License (also |
|
6
|
** known as the "2-Clause License" or "FreeBSD License".) |
|
7
|
** |
|
8
|
** This program is distributed in the hope that it will be useful, |
|
9
|
** but without any warranty; without even the implied warranty of |
|
10
|
** merchantability or fitness for a particular purpose. |
|
11
|
** |
|
12
|
** Author contact information: |
|
13
|
** [email protected] |
|
14
|
** http://www.hwaci.com/drh/ |
|
15
|
** |
|
16
|
******************************************************************************* |
|
17
|
** |
|
18
|
** This file contains code for the /fileedit page and related bits. |
|
19
|
*/ |
|
20
|
#include "config.h" |
|
21
|
#include "fileedit.h" |
|
22
|
#include <assert.h> |
|
23
|
#include <stdarg.h> |
|
24
|
|
|
25
|
/* |
|
26
|
** State for the "mini-checkin" infrastructure, which enables the |
|
27
|
** ability to commit changes to a single file without a check-out |
|
28
|
** db, e.g. for use via an HTTP request. |
|
29
|
** |
|
30
|
** Use CheckinMiniInfo_init() to cleanly initialize one to a known |
|
31
|
** valid/empty default state. |
|
32
|
** |
|
33
|
** Memory for all non-const pointer members is owned by the |
|
34
|
** CheckinMiniInfo instance, unless explicitly noted otherwise, and is |
|
35
|
** freed by CheckinMiniInfo_cleanup(). Similarly, each instance owns |
|
36
|
** any memory for its own Blob members, but NOT for its pointers to |
|
37
|
** blobs. |
|
38
|
*/ |
|
39
|
struct CheckinMiniInfo { |
|
40
|
Manifest * pParent; /* parent check-in. Memory is owned by this |
|
41
|
object. */ |
|
42
|
char *zParentUuid; /* Full UUID of pParent */ |
|
43
|
char *zFilename; /* Name of single file to commit. Must be |
|
44
|
relative to the top of the repo. */ |
|
45
|
Blob fileContent; /* Content of file referred to by zFilename. */ |
|
46
|
Blob fileHash; /* Hash of this->fileContent, using the repo's |
|
47
|
preferred hash method. */ |
|
48
|
Blob comment; /* Check-in comment text */ |
|
49
|
char *zCommentMimetype; /* Mimetype of comment. May be NULL */ |
|
50
|
char *zUser; /* User name */ |
|
51
|
char *zDate; /* Optionally force this date string (anything |
|
52
|
supported by date_in_standard_format()). |
|
53
|
May be NULL. */ |
|
54
|
Blob *pMfOut; /* If not NULL, checkin_mini() will write a |
|
55
|
copy of the generated manifest here. This |
|
56
|
memory is NOT owned by CheckinMiniInfo. */ |
|
57
|
int filePerm; /* Permissions (via file_perm()) of the input |
|
58
|
file. We need to store this before calling |
|
59
|
checkin_mini() because the real input file |
|
60
|
name may differ from the repo-centric |
|
61
|
this->zFilename, and checkin_mini() requires |
|
62
|
the permissions of the original file. For |
|
63
|
web commits, set this to PERM_REG or (when |
|
64
|
editing executable scripts) PERM_EXE before |
|
65
|
calling checkin_mini(). */ |
|
66
|
int flags; /* Bitmask of fossil_cimini_flags. */ |
|
67
|
}; |
|
68
|
typedef struct CheckinMiniInfo CheckinMiniInfo; |
|
69
|
|
|
70
|
/* |
|
71
|
** CheckinMiniInfo::flags values. |
|
72
|
*/ |
|
73
|
enum fossil_cimini_flags { |
|
74
|
/* |
|
75
|
** Must have a value of 0. All other flags have unspecified values. |
|
76
|
*/ |
|
77
|
CIMINI_NONE = 0, |
|
78
|
/* |
|
79
|
** Tells checkin_mini() to use dry-run mode. |
|
80
|
*/ |
|
81
|
CIMINI_DRY_RUN = 1, |
|
82
|
/* |
|
83
|
** Tells checkin_mini() to allow forking from a non-leaf commit. |
|
84
|
*/ |
|
85
|
CIMINI_ALLOW_FORK = 1<<1, |
|
86
|
/* |
|
87
|
** Tells checkin_mini() to dump its generated manifest to stdout. |
|
88
|
*/ |
|
89
|
CIMINI_DUMP_MANIFEST = 1<<2, |
|
90
|
|
|
91
|
/* |
|
92
|
** By default, content containing what appears to be a merge conflict |
|
93
|
** marker is not permitted. This flag relaxes that requirement. |
|
94
|
*/ |
|
95
|
CIMINI_ALLOW_MERGE_MARKER = 1<<3, |
|
96
|
|
|
97
|
/* |
|
98
|
** By default mini-checkins are not allowed to be "older" |
|
99
|
** than their parent. i.e. they may not have a timestamp |
|
100
|
** which predates their parent. This flag bypasses that |
|
101
|
** check. |
|
102
|
*/ |
|
103
|
CIMINI_ALLOW_OLDER = 1<<4, |
|
104
|
|
|
105
|
/* |
|
106
|
** Indicates that the content of the newly checked-in file is |
|
107
|
** converted, if needed, to use the same EOL style as the previous |
|
108
|
** version of that file. Only the in-memory/in-repo copies are |
|
109
|
** affected, not the original file (if any). |
|
110
|
*/ |
|
111
|
CIMINI_CONVERT_EOL_INHERIT = 1<<5, |
|
112
|
/* |
|
113
|
** Indicates that the input's EOLs should be converted to Unix-style. |
|
114
|
*/ |
|
115
|
CIMINI_CONVERT_EOL_UNIX = 1<<6, |
|
116
|
/* |
|
117
|
** Indicates that the input's EOLs should be converted to Windows-style. |
|
118
|
*/ |
|
119
|
CIMINI_CONVERT_EOL_WINDOWS = 1<<7, |
|
120
|
/* |
|
121
|
** A hint to checkin_mini() to "prefer" creation of a delta manifest. |
|
122
|
** It may decide not to for various reasons. |
|
123
|
*/ |
|
124
|
CIMINI_PREFER_DELTA = 1<<8, |
|
125
|
/* |
|
126
|
** A "stronger hint" to checkin_mini() to prefer creation of a delta |
|
127
|
** manifest if it at all can. It will decide not to only if creation |
|
128
|
** of a delta is not a realistic option or if it's forbidden by the |
|
129
|
** forbid-delta-manifests repo config option. For this to work, it |
|
130
|
** must be set together with the CIMINI_PREFER_DELTA flag, but the two |
|
131
|
** cannot be combined in this enum. |
|
132
|
** |
|
133
|
** This option is ONLY INTENDED FOR TESTING, used in bypassing |
|
134
|
** heuristics which may otherwise disable generation of a delta on the |
|
135
|
** grounds of efficiency (e.g. not generating a delta if the parent |
|
136
|
** non-delta only has a few F-cards). |
|
137
|
*/ |
|
138
|
CIMINI_STRONGLY_PREFER_DELTA = 1<<9, |
|
139
|
/* |
|
140
|
** Tells checkin_mini() to permit the addition of a new file. Normally |
|
141
|
** this is disabled because there are hypothetically many cases where |
|
142
|
** it could cause the inadvertent addition of a new file when an |
|
143
|
** update to an existing was intended, as a side-effect of name-case |
|
144
|
** differences. |
|
145
|
*/ |
|
146
|
CIMINI_ALLOW_NEW_FILE = 1<<10 |
|
147
|
}; |
|
148
|
|
|
149
|
/* |
|
150
|
** Initializes p to a known-valid default state. |
|
151
|
*/ |
|
152
|
static void CheckinMiniInfo_init( CheckinMiniInfo * p ){ |
|
153
|
memset(p, 0, sizeof(CheckinMiniInfo)); |
|
154
|
p->flags = CIMINI_NONE; |
|
155
|
p->filePerm = -1; |
|
156
|
p->comment = p->fileContent = p->fileHash = empty_blob; |
|
157
|
} |
|
158
|
|
|
159
|
/* |
|
160
|
** Frees all memory owned by p, but does not free p. |
|
161
|
*/ |
|
162
|
static void CheckinMiniInfo_cleanup( CheckinMiniInfo * p ){ |
|
163
|
blob_reset(&p->comment); |
|
164
|
blob_reset(&p->fileContent); |
|
165
|
blob_reset(&p->fileHash); |
|
166
|
if(p->pParent){ |
|
167
|
manifest_destroy(p->pParent); |
|
168
|
} |
|
169
|
fossil_free(p->zFilename); |
|
170
|
fossil_free(p->zDate); |
|
171
|
fossil_free(p->zParentUuid); |
|
172
|
fossil_free(p->zCommentMimetype); |
|
173
|
fossil_free(p->zUser); |
|
174
|
CheckinMiniInfo_init(p); |
|
175
|
} |
|
176
|
|
|
177
|
/* |
|
178
|
** Internal helper which returns an F-card perms string suitable for |
|
179
|
** writing as-is into a manifest. If it's not empty, it includes a |
|
180
|
** leading space to separate it from the F-card's hash field. |
|
181
|
*/ |
|
182
|
static const char * mfile_permint_mstring(int perm){ |
|
183
|
switch(perm){ |
|
184
|
case PERM_EXE: return " x"; |
|
185
|
case PERM_LNK: return " l"; |
|
186
|
default: return ""; |
|
187
|
} |
|
188
|
} |
|
189
|
|
|
190
|
/* |
|
191
|
** Given a ManifestFile permission string (or NULL), it returns one of |
|
192
|
** PERM_REG, PERM_EXE, or PERM_LNK. |
|
193
|
*/ |
|
194
|
static int mfile_permstr_int(const char *zPerm){ |
|
195
|
if(!zPerm || !*zPerm) return PERM_REG; |
|
196
|
else if(strstr(zPerm,"x")) return PERM_EXE; |
|
197
|
else if(strstr(zPerm,"l")) return PERM_LNK; |
|
198
|
else return PERM_REG/*???*/; |
|
199
|
} |
|
200
|
|
|
201
|
/* |
|
202
|
** Internal helper for checkin_mini() and friends. Appends an F-card |
|
203
|
** for p to pOut. |
|
204
|
*/ |
|
205
|
static void checkin_mini_append_fcard(Blob *pOut, |
|
206
|
const ManifestFile *p){ |
|
207
|
if(p->zUuid){ |
|
208
|
assert(*p->zUuid); |
|
209
|
blob_appendf(pOut, "F %F %s%s", p->zName, |
|
210
|
p->zUuid, |
|
211
|
mfile_permint_mstring(manifest_file_mperm(p))); |
|
212
|
if(p->zPrior){ |
|
213
|
assert(*p->zPrior); |
|
214
|
blob_appendf(pOut, " %F\n", p->zPrior); |
|
215
|
}else{ |
|
216
|
blob_append(pOut, "\n", 1); |
|
217
|
} |
|
218
|
}else{ |
|
219
|
/* File was removed from parent delta. */ |
|
220
|
blob_appendf(pOut, "F %F\n", p->zName); |
|
221
|
} |
|
222
|
} |
|
223
|
|
|
224
|
/* |
|
225
|
** Handles the F-card parts for create_manifest_mini(). |
|
226
|
** |
|
227
|
** If asDelta is true, F-cards will be handled as for a delta |
|
228
|
** manifest, and the caller MUST have added a B-card to pOut before |
|
229
|
** calling this. |
|
230
|
** |
|
231
|
** Returns 1 on success, 0 on error, and writes any error message to |
|
232
|
** pErr (if it's not NULL). The only non-immediately-fatal/panic error |
|
233
|
** is if pCI->filePerm is PERM_LNK or pCI would update a PERM_LNK |
|
234
|
** in-repo file. |
|
235
|
*/ |
|
236
|
static int create_manifest_mini_fcards( Blob * pOut, |
|
237
|
CheckinMiniInfo * pCI, |
|
238
|
int asDelta, |
|
239
|
Blob * pErr){ |
|
240
|
int wroteThisCard = 0; |
|
241
|
const ManifestFile * pFile; |
|
242
|
int (*fncmp)(char const *, char const *) = /* filename comparator */ |
|
243
|
filenames_are_case_sensitive() |
|
244
|
? fossil_strcmp |
|
245
|
: fossil_stricmp; |
|
246
|
#define mf_err(EXPR) if(pErr) blob_appendf EXPR; return 0 |
|
247
|
#define write_this_card(NAME) \ |
|
248
|
blob_appendf(pOut, "F %F %b%s\n", (NAME), &pCI->fileHash, \ |
|
249
|
mfile_permint_mstring(pCI->filePerm)); \ |
|
250
|
wroteThisCard = 1 |
|
251
|
|
|
252
|
assert(pCI->filePerm!=PERM_LNK && "This should have been validated before."); |
|
253
|
assert(pCI->filePerm==PERM_REG || pCI->filePerm==PERM_EXE); |
|
254
|
if(PERM_LNK==pCI->filePerm){ |
|
255
|
goto err_no_symlink; |
|
256
|
} |
|
257
|
manifest_file_rewind(pCI->pParent); |
|
258
|
if(asDelta!=0 && (pCI->pParent->zBaseline==0 |
|
259
|
|| pCI->pParent->nFile==0)){ |
|
260
|
/* Parent is a baseline or a delta with no F-cards, so this is |
|
261
|
** the simplest case: create a delta with a single F-card. |
|
262
|
*/ |
|
263
|
pFile = manifest_file_find(pCI->pParent, pCI->zFilename); |
|
264
|
if(pFile!=0 && manifest_file_mperm(pFile)==PERM_LNK){ |
|
265
|
goto err_no_symlink; |
|
266
|
} |
|
267
|
write_this_card(pFile ? pFile->zName : pCI->zFilename); |
|
268
|
return 1; |
|
269
|
} |
|
270
|
while(1){ |
|
271
|
int cmp; |
|
272
|
if(asDelta==0){ |
|
273
|
pFile = manifest_file_next(pCI->pParent, 0); |
|
274
|
}else{ |
|
275
|
/* Parent is a delta manifest with F-cards. Traversal of delta |
|
276
|
** manifest file entries is normally done via |
|
277
|
** manifest_file_next(), which takes into account the |
|
278
|
** differences between the delta and its parent and returns |
|
279
|
** F-cards from both. Each successive delta from the same |
|
280
|
** baseline includes all F-card changes from the previous |
|
281
|
** deltas, so we instead clone the parent's F-cards except for |
|
282
|
** the one (if any) which matches the new file. |
|
283
|
*/ |
|
284
|
pFile = pCI->pParent->iFile < pCI->pParent->nFile |
|
285
|
? &pCI->pParent->aFile[pCI->pParent->iFile++] |
|
286
|
: 0; |
|
287
|
} |
|
288
|
if(0==pFile) break; |
|
289
|
cmp = fncmp(pFile->zName, pCI->zFilename); |
|
290
|
if(cmp<0){ |
|
291
|
checkin_mini_append_fcard(pOut,pFile); |
|
292
|
}else{ |
|
293
|
if(cmp==0 || 0==wroteThisCard){ |
|
294
|
assert(0==wroteThisCard); |
|
295
|
if(PERM_LNK==manifest_file_mperm(pFile)){ |
|
296
|
goto err_no_symlink; |
|
297
|
} |
|
298
|
write_this_card(cmp==0 ? pFile->zName : pCI->zFilename); |
|
299
|
} |
|
300
|
if(cmp>0){ |
|
301
|
assert(wroteThisCard!=0); |
|
302
|
checkin_mini_append_fcard(pOut,pFile); |
|
303
|
} |
|
304
|
} |
|
305
|
} |
|
306
|
if(wroteThisCard==0){ |
|
307
|
write_this_card(pCI->zFilename); |
|
308
|
} |
|
309
|
return 1; |
|
310
|
err_no_symlink: |
|
311
|
mf_err((pErr,"Cannot commit or overwrite symlinks " |
|
312
|
"via mini-checkin.")); |
|
313
|
return 0; |
|
314
|
#undef write_this_card |
|
315
|
#undef mf_err |
|
316
|
} |
|
317
|
|
|
318
|
/* |
|
319
|
** Creates a manifest file, written to pOut, from the state in the |
|
320
|
** fully-populated and semantically valid pCI argument. pCI is not |
|
321
|
** *semantically* modified by this routine but cannot be const because |
|
322
|
** blob_str() may need to NUL-terminate any given blob. |
|
323
|
** |
|
324
|
** Returns true on success. On error, returns 0 and, if pErr is not |
|
325
|
** NULL, writes an error message there. |
|
326
|
** |
|
327
|
** Intended only to be called via checkin_mini() or routines which |
|
328
|
** have already completely vetted pCI for semantic validity. |
|
329
|
*/ |
|
330
|
static int create_manifest_mini( Blob * pOut, CheckinMiniInfo * pCI, |
|
331
|
Blob * pErr){ |
|
332
|
Blob zCard = empty_blob; /* Z-card checksum */ |
|
333
|
int asDelta = 0; |
|
334
|
#define mf_err(EXPR) if(pErr) blob_appendf EXPR; return 0 |
|
335
|
|
|
336
|
assert(blob_str(&pCI->fileHash)); |
|
337
|
assert(pCI->pParent); |
|
338
|
assert(pCI->zFilename); |
|
339
|
assert(pCI->zUser); |
|
340
|
assert(pCI->zDate); |
|
341
|
|
|
342
|
/* Potential TODOs include... |
|
343
|
** |
|
344
|
** - Maybe add support for tags. Those can be edited via /info page, |
|
345
|
** and feel like YAGNI/feature creep for this purpose. |
|
346
|
*/ |
|
347
|
blob_zero(pOut); |
|
348
|
manifest_file_rewind(pCI->pParent) /* force load of baseline */; |
|
349
|
/* Determine whether we want to create a delta manifest... */ |
|
350
|
if((CIMINI_PREFER_DELTA & pCI->flags) |
|
351
|
&& ((CIMINI_STRONGLY_PREFER_DELTA & pCI->flags) |
|
352
|
|| (pCI->pParent->pBaseline |
|
353
|
? pCI->pParent->pBaseline |
|
354
|
: pCI->pParent)->nFile > 15 |
|
355
|
/* 15 is arbitrary: don't create a delta when there is only a |
|
356
|
** tiny gain for doing so. That heuristic is not *quite* |
|
357
|
** right, in that when we're deriving from another delta, we |
|
358
|
** really should compare the F-card count between it and its |
|
359
|
** baseline, and create a delta if the baseline has (say) |
|
360
|
** twice or more as many F-cards as the previous delta. */) |
|
361
|
&& !db_get_boolean("forbid-delta-manifests",0) |
|
362
|
){ |
|
363
|
asDelta = 1; |
|
364
|
blob_appendf(pOut, "B %s\n", |
|
365
|
pCI->pParent->zBaseline |
|
366
|
? pCI->pParent->zBaseline |
|
367
|
: pCI->zParentUuid); |
|
368
|
} |
|
369
|
if(blob_size(&pCI->comment)!=0){ |
|
370
|
blob_appendf(pOut, "C %F\n", blob_str(&pCI->comment)); |
|
371
|
}else{ |
|
372
|
blob_append(pOut, "C (no\\scomment)\n", 16); |
|
373
|
} |
|
374
|
blob_appendf(pOut, "D %s\n", pCI->zDate); |
|
375
|
if(create_manifest_mini_fcards(pOut,pCI,asDelta,pErr)==0){ |
|
376
|
return 0; |
|
377
|
} |
|
378
|
if(pCI->zCommentMimetype!=0 && pCI->zCommentMimetype[0]!=0){ |
|
379
|
blob_appendf(pOut, "N %F\n", pCI->zCommentMimetype); |
|
380
|
} |
|
381
|
blob_appendf(pOut, "P %s\n", pCI->zParentUuid); |
|
382
|
blob_appendf(pOut, "U %F\n", pCI->zUser); |
|
383
|
md5sum_blob(pOut, &zCard); |
|
384
|
blob_appendf(pOut, "Z %b\n", &zCard); |
|
385
|
blob_reset(&zCard); |
|
386
|
return 1; |
|
387
|
#undef mf_err |
|
388
|
} |
|
389
|
|
|
390
|
/* |
|
391
|
** A so-called "single-file/mini/web check-in" is a slimmed-down form |
|
392
|
** of the check-in command which accepts only a single file and is |
|
393
|
** intended to accept edits to a file via the web interface or from |
|
394
|
** the CLI from outside of a check-out. |
|
395
|
** |
|
396
|
** Being fully non-interactive is a requirement for this function, |
|
397
|
** thus it cannot perform autosync or similar activities (which |
|
398
|
** includes checking for repo locks). |
|
399
|
** |
|
400
|
** This routine uses the state from the given fully-populated pCI |
|
401
|
** argument to add pCI->fileContent to the database, and create and |
|
402
|
** save a manifest for that change. Ownership of pCI and its contents |
|
403
|
** are unchanged. |
|
404
|
** |
|
405
|
** This function may modify pCI as follows: |
|
406
|
** |
|
407
|
** - If one of Manifest pCI->pParent or pCI->zParentUuid are NULL, |
|
408
|
** then the other will be assigned based on its counterpart. Both |
|
409
|
** may not be NULL. |
|
410
|
** |
|
411
|
** - pCI->zDate is normalized to/replaced with a valid date/time |
|
412
|
** string. If its original value cannot be validated then |
|
413
|
** this function fails. If pCI->zDate is NULL, the current time |
|
414
|
** is used. |
|
415
|
** |
|
416
|
** - If the CIMINI_CONVERT_EOL_INHERIT flag is set, |
|
417
|
** pCI->fileContent appears to be plain text, and its line-ending |
|
418
|
** style differs from its previous version, it is converted to the |
|
419
|
** same EOL style as the previous version. If this is done, the |
|
420
|
** pCI->fileHash is re-computed. Note that only pCI->fileContent, |
|
421
|
** not the original file, is affected by the conversion. |
|
422
|
** |
|
423
|
** - Else if one of the CIMINI_CONVERT_EOL_WINDOWS or |
|
424
|
** CIMINI_CONVERT_EOL_UNIX flags are set, pCI->fileContent is |
|
425
|
** converted, if needed, to the corresponding EOL style. |
|
426
|
** |
|
427
|
** - If EOL conversion takes place, pCI->fileHash is re-calculated. |
|
428
|
** |
|
429
|
** - If pCI->fileHash is empty, this routine populates it with the |
|
430
|
** repository's preferred hash algorithm (after any EOL conversion). |
|
431
|
** |
|
432
|
** - pCI->comment may be converted to Unix-style newlines. |
|
433
|
** |
|
434
|
** pCI's ownership is not modified. |
|
435
|
** |
|
436
|
** This function validates pCI's state and fails if any validation |
|
437
|
** fails. |
|
438
|
** |
|
439
|
** On error, returns false (0) and, if pErr is not NULL, writes a |
|
440
|
** diagnostic message there. |
|
441
|
** |
|
442
|
** Returns true on success. If pRid is not NULL, the RID of the |
|
443
|
** resulting manifest is written to *pRid. |
|
444
|
** |
|
445
|
** The check-in process is largely influenced by pCI->flags, and that |
|
446
|
** must be populated before calling this. See the fossil_cimini_flags |
|
447
|
** enum for the docs for each flag. |
|
448
|
*/ |
|
449
|
static int checkin_mini(CheckinMiniInfo * pCI, int *pRid, Blob * pErr){ |
|
450
|
Blob mf = empty_blob; /* output manifest */ |
|
451
|
int rid = 0, frid = 0; /* various RIDs */ |
|
452
|
int isPrivate; /* whether this is private content |
|
453
|
or not */ |
|
454
|
ManifestFile * zFilePrev; /* file entry from pCI->pParent */ |
|
455
|
int prevFRid = 0; /* RID of file's prev. version */ |
|
456
|
#define ci_err(EXPR) if(pErr!=0){blob_appendf EXPR;} goto ci_error |
|
457
|
|
|
458
|
db_begin_transaction(); |
|
459
|
if(pCI->pParent==0 && pCI->zParentUuid==0){ |
|
460
|
ci_err((pErr, "Cannot determine parent version.")); |
|
461
|
} |
|
462
|
else if(pCI->pParent==0){ |
|
463
|
pCI->pParent = manifest_get_by_name(pCI->zParentUuid, 0); |
|
464
|
if(pCI->pParent==0){ |
|
465
|
ci_err((pErr,"Cannot load manifest for [%S].", pCI->zParentUuid)); |
|
466
|
} |
|
467
|
}else if(pCI->zParentUuid==0){ |
|
468
|
pCI->zParentUuid = rid_to_uuid(pCI->pParent->rid); |
|
469
|
assert(pCI->zParentUuid); |
|
470
|
} |
|
471
|
assert(pCI->pParent->rid>0); |
|
472
|
if(leaf_is_closed(pCI->pParent->rid)){ |
|
473
|
ci_err((pErr,"Cannot commit to a closed leaf.")); |
|
474
|
/* Remember that in order to override this we'd also need to |
|
475
|
** cancel TAG_CLOSED on pCI->pParent. There would seem to be no |
|
476
|
** reason we can't do that via the generated manifest, but the |
|
477
|
** commit command does not offer that option, so mini-checkin |
|
478
|
** probably shouldn't, either. |
|
479
|
*/ |
|
480
|
} |
|
481
|
if( !db_exists("SELECT 1 FROM user WHERE login=%Q", pCI->zUser) ){ |
|
482
|
ci_err((pErr,"No such user: %s", pCI->zUser)); |
|
483
|
} |
|
484
|
if(!(CIMINI_ALLOW_FORK & pCI->flags) |
|
485
|
&& !is_a_leaf(pCI->pParent->rid)){ |
|
486
|
ci_err((pErr,"Parent [%S] is not a leaf and forking is disabled.", |
|
487
|
pCI->zParentUuid)); |
|
488
|
} |
|
489
|
if(!(CIMINI_ALLOW_MERGE_MARKER & pCI->flags) |
|
490
|
&& contains_merge_marker(&pCI->fileContent)){ |
|
491
|
ci_err((pErr,"Content appears to contain a merge conflict marker.")); |
|
492
|
} |
|
493
|
if(!file_is_simple_pathname(pCI->zFilename, 1)){ |
|
494
|
ci_err((pErr,"Invalid filename for use in a repository: %s", |
|
495
|
pCI->zFilename)); |
|
496
|
} |
|
497
|
if(!(CIMINI_ALLOW_OLDER & pCI->flags) |
|
498
|
&& !checkin_is_younger(pCI->pParent->rid, pCI->zDate)){ |
|
499
|
ci_err((pErr,"Check-in time (%s) may not be older " |
|
500
|
"than its parent (%z).", |
|
501
|
pCI->zDate, |
|
502
|
db_text(0, "SELECT strftime('%%Y-%%m-%%dT%%H:%%M:%%f',%lf)", |
|
503
|
pCI->pParent->rDate) |
|
504
|
)); |
|
505
|
} |
|
506
|
{ |
|
507
|
/* |
|
508
|
** Normalize the timestamp. We don't use date_in_standard_format() |
|
509
|
** because that has side-effects we don't want to trigger here. |
|
510
|
*/ |
|
511
|
char * zDVal = db_text( |
|
512
|
0, "SELECT strftime('%%Y-%%m-%%dT%%H:%%M:%%f',%Q)", |
|
513
|
pCI->zDate ? pCI->zDate : "now"); |
|
514
|
if(zDVal==0 || zDVal[0]==0){ |
|
515
|
fossil_free(zDVal); |
|
516
|
ci_err((pErr,"Invalid timestamp string: %s", pCI->zDate)); |
|
517
|
} |
|
518
|
fossil_free(pCI->zDate); |
|
519
|
pCI->zDate = zDVal; |
|
520
|
} |
|
521
|
{ /* Confirm that only one EOL policy is in place. */ |
|
522
|
int n = 0; |
|
523
|
if(CIMINI_CONVERT_EOL_INHERIT & pCI->flags) ++n; |
|
524
|
if(CIMINI_CONVERT_EOL_UNIX & pCI->flags) ++n; |
|
525
|
if(CIMINI_CONVERT_EOL_WINDOWS & pCI->flags) ++n; |
|
526
|
if(n>1){ |
|
527
|
ci_err((pErr,"More than 1 EOL conversion policy was specified.")); |
|
528
|
} |
|
529
|
} |
|
530
|
/* Potential TODOs include: |
|
531
|
** |
|
532
|
** - Commit allows an empty check-in only with a flag, but we |
|
533
|
** currently disallow an empty check-in entirely. Conform with |
|
534
|
** commit? |
|
535
|
** |
|
536
|
** Non-TODOs: |
|
537
|
** |
|
538
|
** - Check for a commit lock would require auto-sync, which this |
|
539
|
** code cannot do if it's going to be run via a web page. |
|
540
|
*/ |
|
541
|
|
|
542
|
/* |
|
543
|
** Confirm that pCI->zFilename can be found in pCI->pParent. If |
|
544
|
** not, fail unless the CIMINI_ALLOW_NEW_FILE flag is set. This is |
|
545
|
** admittedly an artificial limitation, not strictly necessary. We |
|
546
|
** do it to hopefully reduce the chance of an "oops" where file |
|
547
|
** X/Y/z gets committed as X/Y/Z or X/y/z due to a typo or |
|
548
|
** case-sensitivity mismatch between the user/repo/filesystem, or |
|
549
|
** some such. |
|
550
|
*/ |
|
551
|
manifest_file_rewind(pCI->pParent); |
|
552
|
zFilePrev = manifest_file_find(pCI->pParent, pCI->zFilename); |
|
553
|
if(!(CIMINI_ALLOW_NEW_FILE & pCI->flags) |
|
554
|
&& (!zFilePrev |
|
555
|
|| !zFilePrev->zUuid/*was removed from parent delta manifest*/) |
|
556
|
){ |
|
557
|
ci_err((pErr,"File [%s] not found in manifest [%S]. " |
|
558
|
"Adding new files is currently not permitted.", |
|
559
|
pCI->zFilename, pCI->zParentUuid)); |
|
560
|
}else if(zFilePrev |
|
561
|
&& manifest_file_mperm(zFilePrev)==PERM_LNK){ |
|
562
|
ci_err((pErr,"Cannot save a symlink via a mini-checkin.")); |
|
563
|
} |
|
564
|
if(zFilePrev){ |
|
565
|
prevFRid = fast_uuid_to_rid(zFilePrev->zUuid); |
|
566
|
} |
|
567
|
|
|
568
|
if(((CIMINI_CONVERT_EOL_INHERIT & pCI->flags) |
|
569
|
|| (CIMINI_CONVERT_EOL_UNIX & pCI->flags) |
|
570
|
|| (CIMINI_CONVERT_EOL_WINDOWS & pCI->flags)) |
|
571
|
&& blob_size(&pCI->fileContent)>0 |
|
572
|
){ |
|
573
|
/* Convert to the requested EOL style. Note that this inherently |
|
574
|
** runs a risk of breaking content, e.g. string literals which |
|
575
|
** contain embedded newlines. Note that HTML5 specifies that |
|
576
|
** form-submitted TEXTAREA content gets normalized to CRLF-style: |
|
577
|
** |
|
578
|
** https://html.spec.whatwg.org/#the-textarea-element |
|
579
|
*/ |
|
580
|
const int pseudoBinary = LOOK_LONG | LOOK_NUL; |
|
581
|
const int lookFlags = LOOK_CRLF | LOOK_LONE_LF | pseudoBinary; |
|
582
|
const int lookNew = looks_like_utf8( &pCI->fileContent, lookFlags ); |
|
583
|
if(!(pseudoBinary & lookNew)){ |
|
584
|
int rehash = 0; |
|
585
|
/*fossil_print("lookNew=%08x\n",lookNew);*/ |
|
586
|
if(CIMINI_CONVERT_EOL_INHERIT & pCI->flags){ |
|
587
|
Blob contentPrev = empty_blob; |
|
588
|
int lookOrig, nOrig; |
|
589
|
content_get(prevFRid, &contentPrev); |
|
590
|
lookOrig = looks_like_utf8(&contentPrev, lookFlags); |
|
591
|
nOrig = blob_size(&contentPrev); |
|
592
|
blob_reset(&contentPrev); |
|
593
|
/*fossil_print("lookOrig=%08x\n",lookOrig);*/ |
|
594
|
if(nOrig>0 && lookOrig!=lookNew){ |
|
595
|
/* If there is a newline-style mismatch, adjust the new |
|
596
|
** content version to the previous style, then re-hash the |
|
597
|
** content. Note that this means that what we insert is NOT |
|
598
|
** what's in the filesystem. |
|
599
|
*/ |
|
600
|
if(!(lookOrig & LOOK_CRLF) && (lookNew & LOOK_CRLF)){ |
|
601
|
/* Old has Unix-style, new has Windows-style. */ |
|
602
|
blob_to_lf_only(&pCI->fileContent); |
|
603
|
rehash = 1; |
|
604
|
}else if((lookOrig & LOOK_CRLF) && !(lookNew & LOOK_CRLF)){ |
|
605
|
/* Old has Windows-style, new has Unix-style. */ |
|
606
|
blob_add_cr(&pCI->fileContent); |
|
607
|
rehash = 1; |
|
608
|
} |
|
609
|
} |
|
610
|
}else{ |
|
611
|
const int oldSize = blob_size(&pCI->fileContent); |
|
612
|
if(CIMINI_CONVERT_EOL_UNIX & pCI->flags){ |
|
613
|
if(LOOK_CRLF & lookNew){ |
|
614
|
blob_to_lf_only(&pCI->fileContent); |
|
615
|
} |
|
616
|
}else{ |
|
617
|
assert(CIMINI_CONVERT_EOL_WINDOWS & pCI->flags); |
|
618
|
if(!(LOOK_CRLF & lookNew)){ |
|
619
|
blob_add_cr(&pCI->fileContent); |
|
620
|
} |
|
621
|
} |
|
622
|
if((int)blob_size(&pCI->fileContent)!=oldSize){ |
|
623
|
rehash = 1; |
|
624
|
} |
|
625
|
} |
|
626
|
if(rehash!=0){ |
|
627
|
hname_hash(&pCI->fileContent, 0, &pCI->fileHash); |
|
628
|
} |
|
629
|
} |
|
630
|
}/* end EOL conversion */ |
|
631
|
|
|
632
|
if(blob_size(&pCI->fileHash)==0){ |
|
633
|
/* Hash the content if it's not done already... */ |
|
634
|
hname_hash(&pCI->fileContent, 0, &pCI->fileHash); |
|
635
|
assert(blob_size(&pCI->fileHash)>0); |
|
636
|
} |
|
637
|
if(zFilePrev){ |
|
638
|
/* Has this file been changed since its previous commit? Note |
|
639
|
** that we have to delay this check until after the potentially |
|
640
|
** expensive EOL conversion. */ |
|
641
|
assert(blob_size(&pCI->fileHash)); |
|
642
|
if(0==fossil_strcmp(zFilePrev->zUuid, blob_str(&pCI->fileHash)) |
|
643
|
&& manifest_file_mperm(zFilePrev)==pCI->filePerm){ |
|
644
|
ci_err((pErr,"File is unchanged. Not committing.")); |
|
645
|
} |
|
646
|
} |
|
647
|
#if 1 |
|
648
|
/* Do we really want to normalize comment EOLs? Web-posting will |
|
649
|
** submit them in CRLF or LF format, depending on how exactly the |
|
650
|
** content is submitted (FORM (CRLF) or textarea-to-POST (LF, at |
|
651
|
** least in theory)). */ |
|
652
|
blob_to_lf_only(&pCI->comment); |
|
653
|
#endif |
|
654
|
/* Create, save, deltify, and crosslink the manifest... */ |
|
655
|
if(create_manifest_mini(&mf, pCI, pErr)==0){ |
|
656
|
return 0; |
|
657
|
} |
|
658
|
isPrivate = content_is_private(pCI->pParent->rid); |
|
659
|
rid = content_put_ex(&mf, 0, 0, 0, isPrivate); |
|
660
|
if(pCI->flags & CIMINI_DUMP_MANIFEST){ |
|
661
|
fossil_print("%b", &mf); |
|
662
|
} |
|
663
|
if(pCI->pMfOut!=0){ |
|
664
|
/* Cross-linking clears mf, so we have to copy it, |
|
665
|
** instead of taking over its memory. */ |
|
666
|
blob_reset(pCI->pMfOut); |
|
667
|
blob_append(pCI->pMfOut, blob_buffer(&mf), blob_size(&mf)); |
|
668
|
} |
|
669
|
content_deltify(rid, &pCI->pParent->rid, 1, 0); |
|
670
|
manifest_crosslink(rid, &mf, 0); |
|
671
|
blob_reset(&mf); |
|
672
|
/* Save and deltify the file content... */ |
|
673
|
frid = content_put_ex(&pCI->fileContent, blob_str(&pCI->fileHash), |
|
674
|
0, 0, isPrivate); |
|
675
|
if(zFilePrev!=0){ |
|
676
|
assert(prevFRid>0); |
|
677
|
content_deltify(frid, &prevFRid, 1, 0); |
|
678
|
} |
|
679
|
db_end_transaction((CIMINI_DRY_RUN & pCI->flags) ? 1 : 0); |
|
680
|
if(pRid!=0){ |
|
681
|
*pRid = rid; |
|
682
|
} |
|
683
|
return 1; |
|
684
|
ci_error: |
|
685
|
assert(db_transaction_nesting_depth()>0); |
|
686
|
db_end_transaction(1); |
|
687
|
return 0; |
|
688
|
#undef ci_err |
|
689
|
} |
|
690
|
|
|
691
|
/* |
|
692
|
** COMMAND: test-ci-mini |
|
693
|
** |
|
694
|
** This is an on-going experiment, subject to change or removal at |
|
695
|
** any time. |
|
696
|
** |
|
697
|
** Usage: %fossil test-ci-mini ?OPTIONS? FILENAME |
|
698
|
** |
|
699
|
** where FILENAME is a repo-relative name as it would appear in the |
|
700
|
** vfile table. |
|
701
|
** |
|
702
|
** Options: |
|
703
|
** -R|--repository REPO The repository file to commit to |
|
704
|
** --as FILENAME The repository-side name of the input |
|
705
|
** file, relative to the top of the |
|
706
|
** repository. Default is the same as the |
|
707
|
** input file name. |
|
708
|
** -m|--comment COMMENT Required check-in comment |
|
709
|
** -M|--comment-file FILE Reads check-in comment from the given file |
|
710
|
** -r|--revision VERSION Commit from this version. Default is |
|
711
|
** the check-out version (if available) or |
|
712
|
** trunk (if used without a check-out). |
|
713
|
** --allow-fork Allows the commit to be made against a |
|
714
|
** non-leaf parent. Note that no autosync |
|
715
|
** is performed beforehand. |
|
716
|
** --allow-merge-conflict Allows check-in of a file even if it |
|
717
|
** appears to contain a fossil merge conflict |
|
718
|
** marker |
|
719
|
** --user-override USER USER to use instead of the current |
|
720
|
** default |
|
721
|
** --date-override DATETIME DATE to use instead of 'now' |
|
722
|
** --allow-older Allow a commit to be older than its |
|
723
|
** ancestor |
|
724
|
** --convert-eol-inherit Convert EOL style of the check-in to match |
|
725
|
** the previous version's content |
|
726
|
** --convert-eol-unix Convert the EOL style to Unix |
|
727
|
** --convert-eol-windows Convert the EOL style to Windows. |
|
728
|
** (Only one of the --convert-eol-X options may be used and they only |
|
729
|
** modified the saved blob, not the input file.) |
|
730
|
** --delta Prefer to generate a delta manifest, if |
|
731
|
** able. The forbid-delta-manifests repo |
|
732
|
** config option trumps this, as do certain |
|
733
|
** heuristics. |
|
734
|
** --allow-new-file Allow addition of a new file this way. |
|
735
|
** Disabled by default to avoid that case- |
|
736
|
** sensitivity errors inadvertently lead to |
|
737
|
** adding a new file where an update is |
|
738
|
** intended. |
|
739
|
** -d|--dump-manifest Dumps the generated manifest to stdout |
|
740
|
** immediately after it's generated |
|
741
|
** --save-manifest FILE Saves the generated manifest to a file |
|
742
|
** after successfully processing it |
|
743
|
** --wet-run Disables the default dry-run mode |
|
744
|
** |
|
745
|
** Example: |
|
746
|
** |
|
747
|
** %fossil test-ci-mini -R REPO -m ... -r foo --as src/myfile.c myfile.c |
|
748
|
** |
|
749
|
*/ |
|
750
|
void test_ci_mini_cmd(void){ |
|
751
|
CheckinMiniInfo cimi; /* check-in state */ |
|
752
|
int newRid = 0; /* RID of new version */ |
|
753
|
const char * zFilename; /* argv[2] */ |
|
754
|
const char * zComment; /* -m comment */ |
|
755
|
const char * zCommentFile; /* -M FILE */ |
|
756
|
const char * zAsFilename; /* --as filename */ |
|
757
|
const char * zRevision; /* -r|--revision [=trunk|checkout] */ |
|
758
|
const char * zUser; /* --user-override */ |
|
759
|
const char * zDate; /* --date-override */ |
|
760
|
char const * zManifestFile = 0;/* --save-manifest FILE */ |
|
761
|
|
|
762
|
/* This function should perform only the minimal "business logic" it |
|
763
|
** needs in order to fully/properly populate the CheckinMiniInfo and |
|
764
|
** then pass it on to checkin_mini() to do most of the validation |
|
765
|
** and work. The point of this is to avoid duplicate code when a web |
|
766
|
** front-end is added for checkin_mini(). |
|
767
|
*/ |
|
768
|
CheckinMiniInfo_init(&cimi); |
|
769
|
zComment = find_option("comment","m",1); |
|
770
|
zCommentFile = find_option("comment-file","M",1); |
|
771
|
zAsFilename = find_option("as",0,1); |
|
772
|
zRevision = find_option("revision","r",1); |
|
773
|
zUser = find_option("user-override",0,1); |
|
774
|
zDate = find_option("date-override",0,1); |
|
775
|
zManifestFile = find_option("save-manifest",0,1); |
|
776
|
if(find_option("wet-run",0,0)==0){ |
|
777
|
cimi.flags |= CIMINI_DRY_RUN; |
|
778
|
} |
|
779
|
if(find_option("allow-fork",0,0)!=0){ |
|
780
|
cimi.flags |= CIMINI_ALLOW_FORK; |
|
781
|
} |
|
782
|
if(find_option("dump-manifest","d",0)!=0){ |
|
783
|
cimi.flags |= CIMINI_DUMP_MANIFEST; |
|
784
|
} |
|
785
|
if(find_option("allow-merge-conflict",0,0)!=0){ |
|
786
|
cimi.flags |= CIMINI_ALLOW_MERGE_MARKER; |
|
787
|
} |
|
788
|
if(find_option("allow-older",0,0)!=0){ |
|
789
|
cimi.flags |= CIMINI_ALLOW_OLDER; |
|
790
|
} |
|
791
|
if(find_option("convert-eol-inherit",0,0)!=0){ |
|
792
|
cimi.flags |= CIMINI_CONVERT_EOL_INHERIT; |
|
793
|
}else if(find_option("convert-eol-unix",0,0)!=0){ |
|
794
|
cimi.flags |= CIMINI_CONVERT_EOL_UNIX; |
|
795
|
}else if(find_option("convert-eol-windows",0,0)!=0){ |
|
796
|
cimi.flags |= CIMINI_CONVERT_EOL_WINDOWS; |
|
797
|
} |
|
798
|
if(find_option("delta",0,0)!=0){ |
|
799
|
cimi.flags |= CIMINI_PREFER_DELTA; |
|
800
|
} |
|
801
|
if(find_option("delta2",0,0)!=0){ |
|
802
|
/* Undocumented. For testing only. */ |
|
803
|
cimi.flags |= CIMINI_PREFER_DELTA | CIMINI_STRONGLY_PREFER_DELTA; |
|
804
|
} |
|
805
|
if(find_option("allow-new-file",0,0)!=0){ |
|
806
|
cimi.flags |= CIMINI_ALLOW_NEW_FILE; |
|
807
|
} |
|
808
|
db_find_and_open_repository(0, 0); |
|
809
|
verify_all_options(); |
|
810
|
user_select(); |
|
811
|
if(g.argc!=3){ |
|
812
|
usage("INFILE"); |
|
813
|
} |
|
814
|
if(zComment && zCommentFile){ |
|
815
|
fossil_fatal("Only one of -m or -M, not both, may be used."); |
|
816
|
}else{ |
|
817
|
if(zCommentFile && *zCommentFile){ |
|
818
|
blob_read_from_file(&cimi.comment, zCommentFile, ExtFILE); |
|
819
|
}else if(zComment && *zComment){ |
|
820
|
blob_append(&cimi.comment, zComment, -1); |
|
821
|
} |
|
822
|
if(!blob_size(&cimi.comment)){ |
|
823
|
fossil_fatal("Non-empty check-in comment is required."); |
|
824
|
} |
|
825
|
} |
|
826
|
db_begin_transaction(); |
|
827
|
zFilename = g.argv[2]; |
|
828
|
cimi.zFilename = mprintf("%/", zAsFilename ? zAsFilename : zFilename); |
|
829
|
cimi.filePerm = file_perm(zFilename, ExtFILE); |
|
830
|
cimi.zUser = fossil_strdup(zUser ? zUser : login_name()); |
|
831
|
if(zDate){ |
|
832
|
cimi.zDate = fossil_strdup(zDate); |
|
833
|
} |
|
834
|
if(zRevision==0 || zRevision[0]==0){ |
|
835
|
if(g.localOpen/*check-out*/){ |
|
836
|
zRevision = db_lget("checkout-hash", 0)/*leak*/; |
|
837
|
}else{ |
|
838
|
zRevision = db_main_branch(); |
|
839
|
} |
|
840
|
} |
|
841
|
name_to_uuid2(zRevision, "ci", &cimi.zParentUuid); |
|
842
|
if(cimi.zParentUuid==0){ |
|
843
|
fossil_fatal("Cannot determine version to commit to."); |
|
844
|
} |
|
845
|
blob_read_from_file(&cimi.fileContent, zFilename, ExtFILE); |
|
846
|
{ |
|
847
|
Blob theManifest = empty_blob; /* --save-manifest target */ |
|
848
|
Blob errMsg = empty_blob; |
|
849
|
int rc; |
|
850
|
if(zManifestFile){ |
|
851
|
cimi.pMfOut = &theManifest; |
|
852
|
} |
|
853
|
rc = checkin_mini(&cimi, &newRid, &errMsg); |
|
854
|
if(rc){ |
|
855
|
assert(blob_size(&errMsg)==0); |
|
856
|
}else{ |
|
857
|
assert(blob_size(&errMsg)); |
|
858
|
fossil_fatal("%b", &errMsg); |
|
859
|
} |
|
860
|
if(zManifestFile){ |
|
861
|
fossil_print("Writing manifest to: %s\n", zManifestFile); |
|
862
|
assert(blob_size(&theManifest)>0); |
|
863
|
blob_write_to_file(&theManifest, zManifestFile); |
|
864
|
blob_reset(&theManifest); |
|
865
|
} |
|
866
|
} |
|
867
|
if(newRid!=0){ |
|
868
|
fossil_print("New version%s: %z\n", |
|
869
|
(cimi.flags & CIMINI_DRY_RUN) ? " (dry run)" : "", |
|
870
|
rid_to_uuid(newRid)); |
|
871
|
} |
|
872
|
db_end_transaction(0/*checkin_mini() will have triggered it to roll |
|
873
|
** back in dry-run mode, but we need access to |
|
874
|
** the transaction-written db state in this |
|
875
|
** routine.*/); |
|
876
|
if(!(cimi.flags & CIMINI_DRY_RUN) && newRid!=0 && g.localOpen!=0){ |
|
877
|
fossil_warning("The check-out state is now out of sync " |
|
878
|
"with regards to this commit. It needs to be " |
|
879
|
"'update'd or 'close'd and re-'open'ed."); |
|
880
|
} |
|
881
|
CheckinMiniInfo_cleanup(&cimi); |
|
882
|
} |
|
883
|
|
|
884
|
/* |
|
885
|
** If the fileedit-glob setting has a value, this returns its Glob |
|
886
|
** object (in memory owned by this function), else it returns NULL. |
|
887
|
*/ |
|
888
|
Glob *fileedit_glob(void){ |
|
889
|
static Glob * pGlobs = 0; |
|
890
|
static int once = 0; |
|
891
|
if(0==pGlobs && once==0){ |
|
892
|
char * zGlobs = db_get("fileedit-glob",0); |
|
893
|
once = 1; |
|
894
|
if(0!=zGlobs && 0!=*zGlobs){ |
|
895
|
pGlobs = glob_create(zGlobs); |
|
896
|
} |
|
897
|
fossil_free(zGlobs); |
|
898
|
} |
|
899
|
return pGlobs; |
|
900
|
} |
|
901
|
|
|
902
|
/* |
|
903
|
** Returns true if the given filename qualifies for online editing by |
|
904
|
** the current user, else returns false. |
|
905
|
** |
|
906
|
** Editing requires that the user have the Write permission and that |
|
907
|
** the filename match the glob defined by the fileedit-glob setting. |
|
908
|
** A missing or empty value for that glob disables all editing. |
|
909
|
*/ |
|
910
|
int fileedit_is_editable(const char *zFilename){ |
|
911
|
Glob * pGlobs = fileedit_glob(); |
|
912
|
if(pGlobs!=0 && zFilename!=0 && *zFilename!=0 && 0!=g.perm.Write){ |
|
913
|
return glob_match(pGlobs, zFilename); |
|
914
|
}else{ |
|
915
|
return 0; |
|
916
|
} |
|
917
|
} |
|
918
|
|
|
919
|
/* |
|
920
|
** Given a repo-relative filename and a manifest RID, returns the UUID |
|
921
|
** of the corresponding file entry. Returns NULL if no match is |
|
922
|
** found. If pFilePerm is not NULL, the file's permission flag value |
|
923
|
** is written to *pFilePerm. |
|
924
|
*/ |
|
925
|
static char *fileedit_file_uuid(char const *zFilename, |
|
926
|
int vid, int *pFilePerm){ |
|
927
|
Stmt stmt = empty_Stmt; |
|
928
|
char * zFileUuid = 0; |
|
929
|
db_prepare(&stmt, "SELECT uuid, perm FROM files_of_checkin " |
|
930
|
"WHERE filename=%Q %s AND checkinID=%d", |
|
931
|
zFilename, filename_collation(), vid); |
|
932
|
if(SQLITE_ROW==db_step(&stmt)){ |
|
933
|
zFileUuid = fossil_strdup(db_column_text(&stmt, 0)); |
|
934
|
if(pFilePerm){ |
|
935
|
*pFilePerm = mfile_permstr_int(db_column_text(&stmt, 1)); |
|
936
|
} |
|
937
|
} |
|
938
|
db_finalize(&stmt); |
|
939
|
return zFileUuid; |
|
940
|
} |
|
941
|
|
|
942
|
/* |
|
943
|
** Returns true if the current user is allowed to edit the given |
|
944
|
** filename, as determined by fileedit_is_editable(), else false, |
|
945
|
** in which case it queues up an error response and the caller |
|
946
|
** must return immediately. |
|
947
|
*/ |
|
948
|
static int fileedit_ajax_check_filename(const char * zFilename){ |
|
949
|
if(0==fileedit_is_editable(zFilename)){ |
|
950
|
ajax_route_error(403, "File is disallowed by the " |
|
951
|
"fileedit-glob setting."); |
|
952
|
return 0; |
|
953
|
} |
|
954
|
return 1; |
|
955
|
} |
|
956
|
|
|
957
|
/* |
|
958
|
** Passed the values of the "checkin" and "filename" request |
|
959
|
** properties, this function verifies that they are valid and |
|
960
|
** populates: |
|
961
|
** |
|
962
|
** - *zRevUuid = the fully-expanded value of zRev (owned by the |
|
963
|
** caller). zRevUuid may be NULL. |
|
964
|
** |
|
965
|
** - *pVid = the RID of zRevUuid. pVid May be NULL. If the vid |
|
966
|
** cannot be resolved or is ambiguous, pVid is not assigned. |
|
967
|
** |
|
968
|
** - *frid = the RID of zFilename's blob content. May not be NULL |
|
969
|
** unless zFilename is also NULL. If BOTH of zFilename and frid are |
|
970
|
** NULL then no confirmation is done on the filename argument - only |
|
971
|
** zRev is checked. |
|
972
|
** |
|
973
|
** Returns 0 if the given file is not in the given check-in or if |
|
974
|
** fileedit_ajax_check_filename() fails, else returns true. If it |
|
975
|
** returns false, it queues up an error response and the caller must |
|
976
|
** return immediately. |
|
977
|
*/ |
|
978
|
static int fileedit_ajax_setup_filerev(const char * zRev, |
|
979
|
char ** zRevUuid, |
|
980
|
int * pVid, |
|
981
|
const char * zFilename, |
|
982
|
int * frid){ |
|
983
|
char * zFileUuid = 0; /* file content UUID */ |
|
984
|
const int checkFile = zFilename!=0 || frid!=0; |
|
985
|
int vid = 0; |
|
986
|
|
|
987
|
if(checkFile && !fileedit_ajax_check_filename(zFilename)){ |
|
988
|
return 0; |
|
989
|
} |
|
990
|
vid = symbolic_name_to_rid(zRev, "ci"); |
|
991
|
if(0==vid){ |
|
992
|
ajax_route_error(404,"Cannot resolve name as a check-in: %s", |
|
993
|
zRev); |
|
994
|
return 0; |
|
995
|
}else if(vid<0){ |
|
996
|
ajax_route_error(400,"Check-in name is ambiguous: %s", |
|
997
|
zRev); |
|
998
|
return 0; |
|
999
|
}else if(pVid!=0){ |
|
1000
|
*pVid = vid; |
|
1001
|
} |
|
1002
|
if(checkFile){ |
|
1003
|
zFileUuid = fileedit_file_uuid(zFilename, vid, 0); |
|
1004
|
if(zFileUuid==0){ |
|
1005
|
ajax_route_error(404, "Check-in does not contain file."); |
|
1006
|
return 0; |
|
1007
|
} |
|
1008
|
} |
|
1009
|
if(zRevUuid!=0){ |
|
1010
|
*zRevUuid = rid_to_uuid(vid); |
|
1011
|
} |
|
1012
|
if(checkFile){ |
|
1013
|
assert(zFileUuid!=0); |
|
1014
|
if(frid!=0){ |
|
1015
|
*frid = fast_uuid_to_rid(zFileUuid); |
|
1016
|
} |
|
1017
|
fossil_free(zFileUuid); |
|
1018
|
} |
|
1019
|
return 1; |
|
1020
|
} |
|
1021
|
|
|
1022
|
/* |
|
1023
|
** AJAX route /fileedit?ajax=content |
|
1024
|
** |
|
1025
|
** Query parameters: |
|
1026
|
** |
|
1027
|
** filename=FILENAME |
|
1028
|
** checkin=CHECKIN_NAME |
|
1029
|
** |
|
1030
|
** User must have Write access to use this page. |
|
1031
|
** |
|
1032
|
** Responds with the raw content of the given page. On error it |
|
1033
|
** produces a JSON response as documented for ajax_route_error(). |
|
1034
|
** |
|
1035
|
** Extra response headers: |
|
1036
|
** |
|
1037
|
** x-fileedit-file-perm: empty or "x" or "l", representing PERM_REG, |
|
1038
|
** PERM_EXE, or PERM_LINK, respectively. |
|
1039
|
** |
|
1040
|
** x-fileedit-checkin-branch: branch name for the passed-in check-in. |
|
1041
|
*/ |
|
1042
|
static void fileedit_ajax_content(void){ |
|
1043
|
const char * zFilename = 0; |
|
1044
|
const char * zRev = 0; |
|
1045
|
int vid, frid; |
|
1046
|
Blob content = empty_blob; |
|
1047
|
const char * zMime; |
|
1048
|
|
|
1049
|
ajax_get_fnci_args( &zFilename, &zRev ); |
|
1050
|
if(!ajax_route_bootstrap(1,0) |
|
1051
|
|| !fileedit_ajax_setup_filerev(zRev, 0, &vid, |
|
1052
|
zFilename, &frid)){ |
|
1053
|
return; |
|
1054
|
} |
|
1055
|
zMime = mimetype_from_name(zFilename); |
|
1056
|
content_get(frid, &content); |
|
1057
|
if(0==zMime){ |
|
1058
|
if(looks_like_binary(&content)){ |
|
1059
|
zMime = "application/octet-stream"; |
|
1060
|
}else{ |
|
1061
|
zMime = "text/plain"; |
|
1062
|
} |
|
1063
|
} |
|
1064
|
{ /* Send the is-exec bit via response header so that the UI can be |
|
1065
|
** updated to account for that. */ |
|
1066
|
int fperm = 0; |
|
1067
|
char * zFuuid = fileedit_file_uuid(zFilename, vid, &fperm); |
|
1068
|
const char * zPerm = mfile_permint_mstring(fperm); |
|
1069
|
assert(zFuuid); |
|
1070
|
cgi_printf_header("x-fileedit-file-perm:%s\r\n", zPerm); |
|
1071
|
fossil_free(zFuuid); |
|
1072
|
} |
|
1073
|
{ /* Send branch name via response header for UI usability reasons */ |
|
1074
|
char * zBranch = branch_of_rid(vid); |
|
1075
|
if(zBranch!=0 && zBranch[0]!=0){ |
|
1076
|
cgi_printf_header("x-fileedit-checkin-branch: %s\r\n", zBranch); |
|
1077
|
} |
|
1078
|
fossil_free(zBranch); |
|
1079
|
} |
|
1080
|
cgi_set_content_type(zMime); |
|
1081
|
cgi_set_content(&content); |
|
1082
|
} |
|
1083
|
|
|
1084
|
/* |
|
1085
|
** AJAX route /fileedit?ajax=diff |
|
1086
|
** |
|
1087
|
** Required query parameters: |
|
1088
|
** |
|
1089
|
** filename=FILENAME |
|
1090
|
** content=text |
|
1091
|
** checkin=check-in version |
|
1092
|
** |
|
1093
|
** Optional parameters: |
|
1094
|
** |
|
1095
|
** sbs=integer (1=side-by-side or 0=unified, default=0) |
|
1096
|
** |
|
1097
|
** ws=integer (0=diff whitespace, 1=ignore EOL ws, 2=ignore all ws) |
|
1098
|
** |
|
1099
|
** Reminder to self: search info.c for isPatch to see how a |
|
1100
|
** patch-style siff can be produced. |
|
1101
|
** |
|
1102
|
** User must have Write access to use this page. |
|
1103
|
** |
|
1104
|
** Responds with the HTML content of the diff. On error it produces a |
|
1105
|
** JSON response as documented for ajax_route_error(). |
|
1106
|
*/ |
|
1107
|
static void fileedit_ajax_diff(void){ |
|
1108
|
/* |
|
1109
|
** Reminder: we only need the filename to perform valdiation |
|
1110
|
** against fileedit_is_editable(), else this route could be |
|
1111
|
** abused to get diffs against content disallowed by the |
|
1112
|
** whitelist. |
|
1113
|
*/ |
|
1114
|
const char * zFilename = 0; |
|
1115
|
const char * zRev = 0; |
|
1116
|
const char * zContent = P("content"); |
|
1117
|
char * zRevUuid = 0; |
|
1118
|
int vid, frid, iFlag; |
|
1119
|
u64 diffFlags = DIFF_HTML | DIFF_NOTTOOBIG; |
|
1120
|
Blob content = empty_blob; |
|
1121
|
|
|
1122
|
iFlag = atoi(PD("sbs","0")); |
|
1123
|
if(0==iFlag){ |
|
1124
|
diffFlags |= DIFF_LINENO; |
|
1125
|
}else{ |
|
1126
|
diffFlags |= DIFF_SIDEBYSIDE; |
|
1127
|
} |
|
1128
|
iFlag = atoi(PD("ws","2")); |
|
1129
|
if(2==iFlag){ |
|
1130
|
diffFlags |= DIFF_IGNORE_ALLWS; |
|
1131
|
}else if(1==iFlag){ |
|
1132
|
diffFlags |= DIFF_IGNORE_EOLWS; |
|
1133
|
} |
|
1134
|
diffFlags |= DIFF_STRIP_EOLCR; |
|
1135
|
ajax_get_fnci_args( &zFilename, &zRev ); |
|
1136
|
if(!ajax_route_bootstrap(1,1) |
|
1137
|
|| !fileedit_ajax_setup_filerev(zRev, &zRevUuid, &vid, |
|
1138
|
zFilename, &frid)){ |
|
1139
|
return; |
|
1140
|
} |
|
1141
|
if(!zContent){ |
|
1142
|
zContent = ""; |
|
1143
|
} |
|
1144
|
cgi_set_content_type("text/html"); |
|
1145
|
blob_init(&content, zContent, -1); |
|
1146
|
{ |
|
1147
|
Blob orig = empty_blob; |
|
1148
|
char * const zOrigUuid = rid_to_uuid(frid); |
|
1149
|
content_get(frid, &orig); |
|
1150
|
ajax_render_diff(&orig, zOrigUuid, &content, diffFlags); |
|
1151
|
fossil_free(zOrigUuid); |
|
1152
|
blob_reset(&orig); |
|
1153
|
} |
|
1154
|
fossil_free(zRevUuid); |
|
1155
|
blob_reset(&content); |
|
1156
|
} |
|
1157
|
|
|
1158
|
/* |
|
1159
|
** Sets up and validates most, but not all, of p's check-in-related |
|
1160
|
** state from the CGI environment. Returns 0 on success or a suggested |
|
1161
|
** HTTP result code on error, in which case a message will have been |
|
1162
|
** written to pErr. |
|
1163
|
** |
|
1164
|
** It always fails if it cannot completely resolve the 'file' and 'r' |
|
1165
|
** parameters, including verifying that the refer to a real |
|
1166
|
** file/version combination and editable by the current user. All |
|
1167
|
** others are optional (at this level, anyway, but upstream code might |
|
1168
|
** require them). |
|
1169
|
** |
|
1170
|
** If the 3rd argument is not NULL and an error is related to a |
|
1171
|
** missing arg then *bIsMissingArg is set to true. This is |
|
1172
|
** intended to allow /fileedit to squelch certain initialization |
|
1173
|
** errors. |
|
1174
|
** |
|
1175
|
** Intended to be used only by /filepage and /filepage_commit. |
|
1176
|
*/ |
|
1177
|
static int fileedit_setup_cimi_from_p(CheckinMiniInfo * p, Blob * pErr, |
|
1178
|
int * bIsMissingArg){ |
|
1179
|
char * zFileUuid = 0; /* UUID of file content */ |
|
1180
|
const char * zFlag; /* generic flag */ |
|
1181
|
int rc = 0, vid = 0, frid = 0; /* result code, check-in/file rids */ |
|
1182
|
|
|
1183
|
#define fail(EXPR) blob_appendf EXPR; goto end_fail |
|
1184
|
zFlag = PD("filename",P("fn")); |
|
1185
|
if(zFlag==0 || !*zFlag){ |
|
1186
|
rc = 400; |
|
1187
|
if(bIsMissingArg){ |
|
1188
|
*bIsMissingArg = 1; |
|
1189
|
} |
|
1190
|
fail((pErr,"Missing required 'filename' parameter.")); |
|
1191
|
} |
|
1192
|
p->zFilename = fossil_strdup(zFlag); |
|
1193
|
|
|
1194
|
if(0==fileedit_is_editable(p->zFilename)){ |
|
1195
|
rc = 403; |
|
1196
|
fail((pErr,"Filename [%h] is disallowed " |
|
1197
|
"by the [fileedit-glob] repository " |
|
1198
|
"setting.", |
|
1199
|
p->zFilename)); |
|
1200
|
} |
|
1201
|
|
|
1202
|
zFlag = PD("checkin",P("ci")); |
|
1203
|
if(!zFlag){ |
|
1204
|
rc = 400; |
|
1205
|
if(bIsMissingArg){ |
|
1206
|
*bIsMissingArg = 1; |
|
1207
|
} |
|
1208
|
fail((pErr,"Missing required 'checkin' parameter.")); |
|
1209
|
} |
|
1210
|
vid = symbolic_name_to_rid(zFlag, "ci"); |
|
1211
|
if(0==vid){ |
|
1212
|
rc = 404; |
|
1213
|
fail((pErr,"Could not resolve check-in version.")); |
|
1214
|
}else if(vid<0){ |
|
1215
|
rc = 400; |
|
1216
|
fail((pErr,"Check-in name is ambiguous.")); |
|
1217
|
} |
|
1218
|
p->zParentUuid = rid_to_uuid(vid)/*fully expand it*/; |
|
1219
|
|
|
1220
|
zFileUuid = fileedit_file_uuid(p->zFilename, vid, &p->filePerm); |
|
1221
|
if(!zFileUuid){ |
|
1222
|
rc = 404; |
|
1223
|
fail((pErr,"Check-in [%S] does not contain file: " |
|
1224
|
"[%h]", p->zParentUuid, p->zFilename)); |
|
1225
|
}else if(PERM_LNK==p->filePerm){ |
|
1226
|
rc = 400; |
|
1227
|
fail((pErr,"Editing symlinks is not permitted.")); |
|
1228
|
} |
|
1229
|
|
|
1230
|
/* Find the repo-side file entry or fail... */ |
|
1231
|
frid = fast_uuid_to_rid(zFileUuid); |
|
1232
|
assert(frid); |
|
1233
|
|
|
1234
|
/* Read file content from submit request or repo... */ |
|
1235
|
zFlag = P("content"); |
|
1236
|
if(zFlag==0){ |
|
1237
|
content_get(frid, &p->fileContent); |
|
1238
|
}else{ |
|
1239
|
blob_init(&p->fileContent,zFlag,-1); |
|
1240
|
} |
|
1241
|
if(looks_like_binary(&p->fileContent)){ |
|
1242
|
rc = 400; |
|
1243
|
fail((pErr,"File appears to be binary. Cannot edit: " |
|
1244
|
"[%h]",p->zFilename)); |
|
1245
|
} |
|
1246
|
|
|
1247
|
zFlag = PT("comment"); |
|
1248
|
if(zFlag!=0 && *zFlag!=0){ |
|
1249
|
blob_append(&p->comment, zFlag, -1); |
|
1250
|
} |
|
1251
|
zFlag = P("comment_mimetype"); |
|
1252
|
if(zFlag){ |
|
1253
|
p->zCommentMimetype = fossil_strdup(zFlag); |
|
1254
|
zFlag = 0; |
|
1255
|
} |
|
1256
|
#define p_int(K) atoi(PD(K,"0")) |
|
1257
|
if(p_int("dry_run")!=0){ |
|
1258
|
p->flags |= CIMINI_DRY_RUN; |
|
1259
|
} |
|
1260
|
if(p_int("allow_fork")!=0){ |
|
1261
|
p->flags |= CIMINI_ALLOW_FORK; |
|
1262
|
} |
|
1263
|
if(p_int("allow_older")!=0){ |
|
1264
|
p->flags |= CIMINI_ALLOW_OLDER; |
|
1265
|
} |
|
1266
|
if(0==p_int("exec_bit")){ |
|
1267
|
p->filePerm = PERM_REG; |
|
1268
|
}else{ |
|
1269
|
p->filePerm = PERM_EXE; |
|
1270
|
} |
|
1271
|
if(p_int("allow_merge_conflict")!=0){ |
|
1272
|
p->flags |= CIMINI_ALLOW_MERGE_MARKER; |
|
1273
|
} |
|
1274
|
if(p_int("prefer_delta")!=0){ |
|
1275
|
p->flags |= CIMINI_PREFER_DELTA; |
|
1276
|
} |
|
1277
|
|
|
1278
|
/* EOL conversion policy... */ |
|
1279
|
switch(p_int("eol")){ |
|
1280
|
case 1: p->flags |= CIMINI_CONVERT_EOL_UNIX; break; |
|
1281
|
case 2: p->flags |= CIMINI_CONVERT_EOL_WINDOWS; break; |
|
1282
|
default: p->flags |= CIMINI_CONVERT_EOL_INHERIT; break; |
|
1283
|
} |
|
1284
|
#undef p_int |
|
1285
|
/* |
|
1286
|
** TODO?: date-override date selection field. Maybe use |
|
1287
|
** an input[type=datetime-local]. |
|
1288
|
*/ |
|
1289
|
p->zUser = fossil_strdup(g.zLogin); |
|
1290
|
return 0; |
|
1291
|
end_fail: |
|
1292
|
#undef fail |
|
1293
|
fossil_free(zFileUuid); |
|
1294
|
return rc ? rc : 500; |
|
1295
|
} |
|
1296
|
|
|
1297
|
/* |
|
1298
|
** Renders a list of all open leaves in JSON form: |
|
1299
|
** |
|
1300
|
** [ |
|
1301
|
** {checkin: UUID, branch: branchName, timestamp: string} |
|
1302
|
** ] |
|
1303
|
** |
|
1304
|
** The entries are ordered newest first. |
|
1305
|
** |
|
1306
|
** If zFirstUuid is not NULL then *zFirstUuid is set to a copy of the |
|
1307
|
** full UUID of the first (most recent) leaf, which must be freed by |
|
1308
|
** the caller. It is set to 0 if there are no leaves. |
|
1309
|
*/ |
|
1310
|
static void fileedit_render_leaves_list(char ** zFirstUuid){ |
|
1311
|
Blob sql = empty_blob; |
|
1312
|
Stmt q = empty_Stmt; |
|
1313
|
int i = 0; |
|
1314
|
|
|
1315
|
if(zFirstUuid){ |
|
1316
|
*zFirstUuid = 0; |
|
1317
|
} |
|
1318
|
blob_append(&sql, timeline_query_for_tty(), -1); |
|
1319
|
blob_append_sql(&sql, " AND blob.rid IN (SElECT rid FROM leaf " |
|
1320
|
"WHERE NOT EXISTS(" |
|
1321
|
"SELECT 1 from tagxref WHERE tagid=%d AND " |
|
1322
|
"tagtype>0 AND rid=leaf.rid" |
|
1323
|
")) " |
|
1324
|
"ORDER BY mtime DESC", TAG_CLOSED); |
|
1325
|
db_prepare_blob(&q, &sql); |
|
1326
|
CX("["); |
|
1327
|
while( SQLITE_ROW==db_step(&q) ){ |
|
1328
|
const char * zUuid = db_column_text(&q, 1); |
|
1329
|
if(i++){ |
|
1330
|
CX(","); |
|
1331
|
}else if(zFirstUuid){ |
|
1332
|
*zFirstUuid = fossil_strdup(zUuid); |
|
1333
|
} |
|
1334
|
CX("{"); |
|
1335
|
CX("\"checkin\":%!j,", zUuid); |
|
1336
|
CX("\"branch\":%!j,", db_column_text(&q, 7)); |
|
1337
|
CX("\"timestamp\":%!j", db_column_text(&q, 2)); |
|
1338
|
CX("}"); |
|
1339
|
} |
|
1340
|
CX("]"); |
|
1341
|
db_finalize(&q); |
|
1342
|
} |
|
1343
|
|
|
1344
|
/* |
|
1345
|
** For the given fully resolved UUID, renders a JSON object containing |
|
1346
|
** the fileeedit-editable files in that check-in: |
|
1347
|
** |
|
1348
|
** { |
|
1349
|
** checkin: UUID, |
|
1350
|
** editableFiles: [ filename1, ... filenameN ] |
|
1351
|
** } |
|
1352
|
** |
|
1353
|
** They are sorted by name using filename_collation(). |
|
1354
|
*/ |
|
1355
|
static void fileedit_render_checkin_files(const char * zFullUuid){ |
|
1356
|
Blob sql = empty_blob; |
|
1357
|
Stmt q = empty_Stmt; |
|
1358
|
int i = 0; |
|
1359
|
|
|
1360
|
CX("{\"checkin\":%!j," |
|
1361
|
"\"editableFiles\":[", zFullUuid); |
|
1362
|
blob_append_sql(&sql, "SELECT filename FROM files_of_checkin(%Q) " |
|
1363
|
"ORDER BY filename %s", |
|
1364
|
zFullUuid, filename_collation()); |
|
1365
|
db_prepare_blob(&q, &sql); |
|
1366
|
while( SQLITE_ROW==db_step(&q) ){ |
|
1367
|
const char * zFilename = db_column_text(&q, 0); |
|
1368
|
if(fileedit_is_editable(zFilename)){ |
|
1369
|
if(i++){ |
|
1370
|
CX(","); |
|
1371
|
} |
|
1372
|
CX("%!j", zFilename); |
|
1373
|
} |
|
1374
|
} |
|
1375
|
db_finalize(&q); |
|
1376
|
CX("]}"); |
|
1377
|
} |
|
1378
|
|
|
1379
|
/* |
|
1380
|
** AJAX route /fileedit?ajax=filelist |
|
1381
|
** |
|
1382
|
** Fetches a JSON-format list of leaves and/or filenames for use in |
|
1383
|
** creating a file selection list in /fileedit. It has different modes |
|
1384
|
** of operation depending on its arguments: |
|
1385
|
** |
|
1386
|
** 'leaves': just fetch a list of open leaf versions, in this |
|
1387
|
** format: |
|
1388
|
** |
|
1389
|
** [ |
|
1390
|
** {checkin: UUID, branch: branchName, timestamp: string} |
|
1391
|
** ] |
|
1392
|
** |
|
1393
|
** The entries are ordered newest first. |
|
1394
|
** |
|
1395
|
** 'checkin=CHECKIN_NAME': fetch the current list of is-editable files |
|
1396
|
** for the current user and given check-in name: |
|
1397
|
** |
|
1398
|
** { |
|
1399
|
** checkin: UUID, |
|
1400
|
** editableFiles: [ filename1, ... filenameN ] // sorted by name |
|
1401
|
** } |
|
1402
|
** |
|
1403
|
** On error it produces a JSON response as documented for |
|
1404
|
** ajax_route_error(). |
|
1405
|
*/ |
|
1406
|
static void fileedit_ajax_filelist(){ |
|
1407
|
const char * zCi = PD("checkin",P("ci")); |
|
1408
|
|
|
1409
|
if(!ajax_route_bootstrap(1,0)){ |
|
1410
|
return; |
|
1411
|
} |
|
1412
|
cgi_set_content_type("application/json"); |
|
1413
|
if(zCi!=0){ |
|
1414
|
char * zCiFull = 0; |
|
1415
|
if(0==fileedit_ajax_setup_filerev(zCi, &zCiFull, 0, 0, 0)){ |
|
1416
|
/* Error already reported */ |
|
1417
|
return; |
|
1418
|
} |
|
1419
|
fileedit_render_checkin_files(zCiFull); |
|
1420
|
fossil_free(zCiFull); |
|
1421
|
}else if(P("leaves")!=0){ |
|
1422
|
fileedit_render_leaves_list(0); |
|
1423
|
}else{ |
|
1424
|
ajax_route_error(500, "Unhandled URL argument."); |
|
1425
|
} |
|
1426
|
} |
|
1427
|
|
|
1428
|
/* |
|
1429
|
** AJAX route /fileedit?ajax=commit |
|
1430
|
** |
|
1431
|
** Required query parameters: |
|
1432
|
** |
|
1433
|
** filename=FILENAME |
|
1434
|
** checkin=Parent check-in UUID |
|
1435
|
** content=text |
|
1436
|
** comment=non-empty text |
|
1437
|
** |
|
1438
|
** Optional query parameters: |
|
1439
|
** |
|
1440
|
** comment_mimetype=text (NOT currently honored) |
|
1441
|
** |
|
1442
|
** dry_run=int (1 or 0) |
|
1443
|
** |
|
1444
|
** include_manifest=int (1 or 0), whether to include |
|
1445
|
** the generated manifest in the response. |
|
1446
|
** |
|
1447
|
** |
|
1448
|
** User must have Write permissions to use this page. |
|
1449
|
** |
|
1450
|
** Responds with JSON (with some state repeated |
|
1451
|
** from the input in order to avoid certain race conditions |
|
1452
|
** client-side): |
|
1453
|
** |
|
1454
|
** { |
|
1455
|
** checkin: newUUID, |
|
1456
|
** filename: theFilename, |
|
1457
|
** mimetype: string, |
|
1458
|
** branch: name of the check-in's branch, |
|
1459
|
** isExe: bool, |
|
1460
|
** dryRun: bool, |
|
1461
|
** manifest: text of manifest, |
|
1462
|
** } |
|
1463
|
** |
|
1464
|
** On error it produces a JSON response as documented for |
|
1465
|
** ajax_route_error(). |
|
1466
|
*/ |
|
1467
|
static void fileedit_ajax_commit(void){ |
|
1468
|
Blob err = empty_blob; /* Error messages */ |
|
1469
|
Blob manifest = empty_blob; /* raw new manifest */ |
|
1470
|
CheckinMiniInfo cimi; /* check-in state */ |
|
1471
|
int rc; /* generic result code */ |
|
1472
|
int newVid = 0; /* new version's RID */ |
|
1473
|
char * zNewUuid = 0; /* newVid's UUID */ |
|
1474
|
char const * zMimetype; |
|
1475
|
char * zBranch = 0; |
|
1476
|
|
|
1477
|
if(!ajax_route_bootstrap(1,1)){ |
|
1478
|
return; |
|
1479
|
} |
|
1480
|
db_begin_transaction(); |
|
1481
|
CheckinMiniInfo_init(&cimi); |
|
1482
|
rc = fileedit_setup_cimi_from_p(&cimi, &err, 0); |
|
1483
|
if(0!=rc){ |
|
1484
|
ajax_route_error(rc,"%b",&err); |
|
1485
|
goto end_cleanup; |
|
1486
|
} |
|
1487
|
if(blob_size(&cimi.comment)==0){ |
|
1488
|
ajax_route_error(400,"Empty check-in comment is not permitted."); |
|
1489
|
goto end_cleanup; |
|
1490
|
} |
|
1491
|
if(0!=atoi(PD("include_manifest","0"))){ |
|
1492
|
cimi.pMfOut = &manifest; |
|
1493
|
} |
|
1494
|
checkin_mini(&cimi, &newVid, &err); |
|
1495
|
if(blob_size(&err)){ |
|
1496
|
ajax_route_error(500,"%b",&err); |
|
1497
|
goto end_cleanup; |
|
1498
|
} |
|
1499
|
assert(newVid>0); |
|
1500
|
zNewUuid = rid_to_uuid(newVid); |
|
1501
|
cgi_set_content_type("application/json"); |
|
1502
|
CX("{"); |
|
1503
|
CX("\"checkin\":%!j,", zNewUuid); |
|
1504
|
CX("\"filename\":%!j,", cimi.zFilename); |
|
1505
|
CX("\"isExe\": %s,", cimi.filePerm==PERM_EXE ? "true" : "false"); |
|
1506
|
zMimetype = mimetype_from_name(cimi.zFilename); |
|
1507
|
if(zMimetype!=0){ |
|
1508
|
CX("\"mimetype\": %!j,", zMimetype); |
|
1509
|
} |
|
1510
|
zBranch = branch_of_rid(newVid); |
|
1511
|
if(zBranch!=0){ |
|
1512
|
CX("\"branch\": %!j,", zBranch); |
|
1513
|
fossil_free(zBranch); |
|
1514
|
} |
|
1515
|
CX("\"dryRun\": %s", |
|
1516
|
(CIMINI_DRY_RUN & cimi.flags) ? "true" : "false"); |
|
1517
|
if(blob_size(&manifest)>0){ |
|
1518
|
CX(",\"manifest\": %!j", blob_str(&manifest)); |
|
1519
|
} |
|
1520
|
CX("}"); |
|
1521
|
end_cleanup: |
|
1522
|
db_end_transaction(0/*noting that dry-run mode will have already |
|
1523
|
** set this to rollback mode. */); |
|
1524
|
fossil_free(zNewUuid); |
|
1525
|
blob_reset(&err); |
|
1526
|
blob_reset(&manifest); |
|
1527
|
CheckinMiniInfo_cleanup(&cimi); |
|
1528
|
} |
|
1529
|
|
|
1530
|
/* |
|
1531
|
** WEBPAGE: fileedit |
|
1532
|
** |
|
1533
|
** Enables the online editing and committing of text files. Requires |
|
1534
|
** that the user have Write permissions and that a user with setup |
|
1535
|
** permissions has set the fileedit-glob setting to a list of glob |
|
1536
|
** patterns matching files which may be edited (e.g. "*.wiki,*.md"). |
|
1537
|
** Note that fileedit-glob, by design, is a local-only setting. |
|
1538
|
** It does not sync across repository clones, and must be explicitly |
|
1539
|
** set on any repositories where this page should be activated. |
|
1540
|
** |
|
1541
|
** Optional query parameters: |
|
1542
|
** |
|
1543
|
** filename=FILENAME Repo-relative path to the file. |
|
1544
|
** checkin=VERSION Check-in version, using any unambiguous |
|
1545
|
** symbolic version name. |
|
1546
|
** |
|
1547
|
** If passed a filename but no check-in then it will attempt to |
|
1548
|
** load that file from the most recent leaf check-in. |
|
1549
|
** |
|
1550
|
** Once the page is loaded, files may be selected from any open leaf |
|
1551
|
** version. The only way to edit files from non-leaf checkins is to |
|
1552
|
** pass both the filename and check-in as URL parameters to the page. |
|
1553
|
** Users with the proper permissions will be presented with "Edit" |
|
1554
|
** links in various file-specific contexts for files which match the |
|
1555
|
** fileedit-glob, regardless of whether they refer to leaf versions or |
|
1556
|
** not. |
|
1557
|
*/ |
|
1558
|
void fileedit_page(void){ |
|
1559
|
const char * zFileMime = 0; /* File mime type guess */ |
|
1560
|
CheckinMiniInfo cimi; /* Check-in state */ |
|
1561
|
int previewRenderMode = AJAX_RENDER_GUESS; /* preview mode */ |
|
1562
|
Blob err = empty_blob; /* Error report */ |
|
1563
|
const char *zAjax = P("name"); /* Name of AJAX route for |
|
1564
|
sub-dispatching. */ |
|
1565
|
|
|
1566
|
/* |
|
1567
|
** Internal-use URL parameters: |
|
1568
|
** |
|
1569
|
** name=string The name of a page-specific AJAX operation. |
|
1570
|
** |
|
1571
|
** Noting that fossil internally stores all URL path components |
|
1572
|
** after the first as the "name" value. Thus /fileedit?name=blah is |
|
1573
|
** equivalent to /fileedit/blah. The latter is the preferred |
|
1574
|
** form. This means, however, that no fileedit ajax routes may make |
|
1575
|
** use of the name parameter. |
|
1576
|
** |
|
1577
|
** Which additional parameters are used by each distinct ajax route |
|
1578
|
** is an internal implementation detail and may change with any |
|
1579
|
** given build of this code. An unknown "name" value triggers an |
|
1580
|
** error, as documented for ajax_route_error(). |
|
1581
|
*/ |
|
1582
|
|
|
1583
|
/* Allow no access to this page without check-in privilege */ |
|
1584
|
login_check_credentials(); |
|
1585
|
if( !g.perm.Write ){ |
|
1586
|
if(zAjax!=0){ |
|
1587
|
ajax_route_error(403, "Write permissions required."); |
|
1588
|
}else{ |
|
1589
|
login_needed(g.anon.Write); |
|
1590
|
} |
|
1591
|
return; |
|
1592
|
} |
|
1593
|
/* No access to anything on this page if the fileedit-glob is empty */ |
|
1594
|
if( fileedit_glob()==0 ){ |
|
1595
|
if(zAjax!=0){ |
|
1596
|
ajax_route_error(403, "Online editing is disabled for this " |
|
1597
|
"repository."); |
|
1598
|
return; |
|
1599
|
} |
|
1600
|
style_header("File Editor (disabled)"); |
|
1601
|
CX("<h1>Online File Editing Is Disabled</h1>\n"); |
|
1602
|
if( g.perm.Admin ){ |
|
1603
|
CX("<p>To enable online editing, the " |
|
1604
|
"<a href='%R/setup_settings'>" |
|
1605
|
"<code>fileedit-glob</code> repository setting</a>\n" |
|
1606
|
"must be set to a comma- and/or newine-delimited list of glob\n" |
|
1607
|
"values matching files which may be edited online." |
|
1608
|
"</p>\n"); |
|
1609
|
}else{ |
|
1610
|
CX("<p>Online editing is disabled for this repository.</p>\n"); |
|
1611
|
} |
|
1612
|
style_finish_page(); |
|
1613
|
return; |
|
1614
|
} |
|
1615
|
|
|
1616
|
/* Dispatch AJAX methods based tail of the request URI. |
|
1617
|
** The AJAX parts do their own permissions/CSRF check and |
|
1618
|
** fail with a JSON-format response if needed. |
|
1619
|
*/ |
|
1620
|
if( 0!=zAjax ){ |
|
1621
|
/* preview mode is handled via /ajax/preview-text */ |
|
1622
|
if(0==strcmp("content",zAjax)){ |
|
1623
|
fileedit_ajax_content(); |
|
1624
|
}else if(0==strcmp("filelist",zAjax)){ |
|
1625
|
fileedit_ajax_filelist(); |
|
1626
|
}else if(0==strcmp("diff",zAjax)){ |
|
1627
|
fileedit_ajax_diff(); |
|
1628
|
}else if(0==strcmp("commit",zAjax)){ |
|
1629
|
fileedit_ajax_commit(); |
|
1630
|
}else{ |
|
1631
|
ajax_route_error(500, "Unhandled ajax route name."); |
|
1632
|
} |
|
1633
|
return; |
|
1634
|
} |
|
1635
|
|
|
1636
|
db_begin_transaction(); |
|
1637
|
CheckinMiniInfo_init(&cimi); |
|
1638
|
style_header("File Editor"); |
|
1639
|
style_emit_noscript_for_js_page(); |
|
1640
|
/* As of this point, don't use return or fossil_fatal(). Write any |
|
1641
|
** error in (&err) and goto end_footer instead so that we can be |
|
1642
|
** sure to emit the error message, do any cleanup, and end the |
|
1643
|
** transaction cleanly. |
|
1644
|
*/ |
|
1645
|
{ |
|
1646
|
int isMissingArg = 0; |
|
1647
|
if(fileedit_setup_cimi_from_p(&cimi, &err, &isMissingArg)==0){ |
|
1648
|
assert(cimi.zFilename); |
|
1649
|
zFileMime = mimetype_from_name(cimi.zFilename); |
|
1650
|
}else if(isMissingArg!=0){ |
|
1651
|
/* Squelch these startup warnings - they're non-fatal now but |
|
1652
|
** used to be fatal. */ |
|
1653
|
blob_reset(&err); |
|
1654
|
} |
|
1655
|
} |
|
1656
|
|
|
1657
|
/******************************************************************** |
|
1658
|
** All errors which "could" have happened up to this point are of a |
|
1659
|
** degree which keep us from rendering the rest of the page, and |
|
1660
|
** thus have already caused us to skipped to the end of the page to |
|
1661
|
** render the errors. Any up-coming errors, barring malloc failure |
|
1662
|
** or similar, are not "that" fatal. We can/should continue |
|
1663
|
** rendering the page, then output the error message at the end. |
|
1664
|
********************************************************************/ |
|
1665
|
|
|
1666
|
/* The CSS for this page lives in a common file but much of it we |
|
1667
|
** don't want inadvertently being used by other pages. We don't |
|
1668
|
** have a common, page-specific container we can filter our CSS |
|
1669
|
** selectors, but we do have the BODY, which we can decorate with |
|
1670
|
** whatever CSS we wish... |
|
1671
|
*/ |
|
1672
|
style_script_begin(__FILE__,__LINE__); |
|
1673
|
CX("document.body.classList.add('fileedit');\n"); |
|
1674
|
style_script_end(); |
|
1675
|
|
|
1676
|
/* Status bar */ |
|
1677
|
CX("<div id='fossil-status-bar' " |
|
1678
|
"title='Status message area. Double-click to clear them.'>" |
|
1679
|
"Status messages will go here.</div>\n" |
|
1680
|
/* will be moved into the tab container via JS */); |
|
1681
|
|
|
1682
|
CX("<div id='fileedit-edit-status'>" |
|
1683
|
"<span class='name'>(no file loaded)</span>" |
|
1684
|
"<span class='links'></span>" |
|
1685
|
"</div>"); |
|
1686
|
|
|
1687
|
/* Main tab container... */ |
|
1688
|
CX("<div id='fileedit-tabs' class='tab-container'></div>"); |
|
1689
|
|
|
1690
|
/* The .hidden class on the following tab elements is to help lessen |
|
1691
|
the FOUC effect of the tabs before JS re-assembles them. */ |
|
1692
|
|
|
1693
|
/***** File/version info tab *****/ |
|
1694
|
{ |
|
1695
|
CX("<div id='fileedit-tab-fileselect' " |
|
1696
|
"data-tab-parent='fileedit-tabs' " |
|
1697
|
"data-tab-label='File Selection' " |
|
1698
|
"class='hidden'" |
|
1699
|
">"); |
|
1700
|
CX("<div id='fileedit-file-selector'></div>"); |
|
1701
|
CX("</div>"/*#fileedit-tab-fileselect*/); |
|
1702
|
} |
|
1703
|
|
|
1704
|
/******* Content tab *******/ |
|
1705
|
{ |
|
1706
|
CX("<div id='fileedit-tab-content' " |
|
1707
|
"data-tab-parent='fileedit-tabs' " |
|
1708
|
"data-tab-label='File Content' " |
|
1709
|
"class='hidden'" |
|
1710
|
">"); |
|
1711
|
CX("<div class='fileedit-options flex-container " |
|
1712
|
"flex-row child-gap-small'>"); |
|
1713
|
CX("<div class='input-with-label'>" |
|
1714
|
"<button class='fileedit-content-reload confirmer' " |
|
1715
|
">Discard & Reload</button>" |
|
1716
|
"<div class='help-buttonlet'>" |
|
1717
|
"Reload the file from the server, discarding " |
|
1718
|
"any local edits. To help avoid accidental loss of " |
|
1719
|
"edits, it requires confirmation (a second click) within " |
|
1720
|
"a few seconds or it will not reload." |
|
1721
|
"</div>" |
|
1722
|
"</div>"); |
|
1723
|
style_select_list_int("select-font-size", |
|
1724
|
"editor_font_size", "Editor font size", |
|
1725
|
NULL/*tooltip*/, |
|
1726
|
100, |
|
1727
|
"100%", 100, "125%", 125, |
|
1728
|
"150%", 150, "175%", 175, |
|
1729
|
"200%", 200, NULL); |
|
1730
|
wikiedit_emit_toggle_preview(); |
|
1731
|
CX("</div>"); |
|
1732
|
CX("<div class='flex-container flex-column stretch'>"); |
|
1733
|
CX("<textarea name='content' id='fileedit-content-editor' " |
|
1734
|
"class='fileedit' rows='25'>"); |
|
1735
|
CX("</textarea>"); |
|
1736
|
CX("</div>"/*textarea wrapper*/); |
|
1737
|
CX("</div>"/*#tab-file-content*/); |
|
1738
|
} |
|
1739
|
|
|
1740
|
/****** Preview tab ******/ |
|
1741
|
{ |
|
1742
|
CX("<div id='fileedit-tab-preview' " |
|
1743
|
"data-tab-parent='fileedit-tabs' " |
|
1744
|
"data-tab-label='Preview' " |
|
1745
|
"class='hidden'" |
|
1746
|
">"); |
|
1747
|
CX("<div class='fileedit-options flex-container flex-row'>"); |
|
1748
|
CX("<button id='btn-preview-refresh' " |
|
1749
|
"data-f-preview-from='fileContent' " |
|
1750
|
/* ^^^ fossil.page[methodName]() OR text source elem ID, |
|
1751
|
** but we need a method in order to support clients swapping out |
|
1752
|
** the text editor with their own. */ |
|
1753
|
"data-f-preview-via='_postPreview' " |
|
1754
|
/* ^^^ fossil.page[methodName](content, callback) */ |
|
1755
|
"data-f-preview-to='_previewTo' " |
|
1756
|
/* ^^^ dest elem ID */ |
|
1757
|
">Refresh</button>"); |
|
1758
|
/* Toggle auto-update of preview when the Preview tab is selected. */ |
|
1759
|
CX("<div class='input-with-label'>" |
|
1760
|
"<input type='checkbox' value='1' " |
|
1761
|
"id='cb-preview-autorefresh' checked>" |
|
1762
|
"<label for='cb-preview-autorefresh'>Auto-refresh?</label>" |
|
1763
|
"<div class='help-buttonlet'>" |
|
1764
|
"If on, the preview will automatically " |
|
1765
|
"refresh (if needed) when this tab is selected." |
|
1766
|
"</div>" |
|
1767
|
"</div>"); |
|
1768
|
|
|
1769
|
/* Default preview rendering mode selection... */ |
|
1770
|
previewRenderMode = zFileMime |
|
1771
|
? ajax_render_mode_for_mimetype(zFileMime) |
|
1772
|
: AJAX_RENDER_GUESS; |
|
1773
|
style_select_list_int("select-preview-mode", |
|
1774
|
"preview_render_mode", |
|
1775
|
"Preview Mode", |
|
1776
|
"Preview mode format.", |
|
1777
|
previewRenderMode, |
|
1778
|
"Guess", AJAX_RENDER_GUESS, |
|
1779
|
"Wiki/Markdown", AJAX_RENDER_WIKI, |
|
1780
|
"HTML (iframe)", AJAX_RENDER_HTML_IFRAME, |
|
1781
|
"HTML (inline)", AJAX_RENDER_HTML_INLINE, |
|
1782
|
"Plain Text", AJAX_RENDER_PLAIN_TEXT, |
|
1783
|
NULL); |
|
1784
|
/* Allow selection of HTML preview iframe height */ |
|
1785
|
style_select_list_int("select-preview-html-ems", |
|
1786
|
"preview_html_ems", |
|
1787
|
"HTML Preview IFrame Height (EMs)", |
|
1788
|
"Height (in EMs) of the iframe used for " |
|
1789
|
"HTML preview", |
|
1790
|
40 /*default*/, |
|
1791
|
"", 20, "", 40, |
|
1792
|
"", 60, "", 80, |
|
1793
|
"", 100, NULL); |
|
1794
|
/* Selection of line numbers for text preview */ |
|
1795
|
style_labeled_checkbox("cb-line-numbers", |
|
1796
|
"preview_ln", |
|
1797
|
"Add line numbers to plain-text previews?", |
|
1798
|
"1", P("preview_ln")!=0, |
|
1799
|
"If on, plain-text files (only) will get " |
|
1800
|
"line numbers added to the preview."); |
|
1801
|
CX("</div>"/*.fileedit-options*/); |
|
1802
|
CX("<div id='fileedit-tab-preview-wrapper'></div>"); |
|
1803
|
CX("</div>"/*#fileedit-tab-preview*/); |
|
1804
|
} |
|
1805
|
|
|
1806
|
/****** Diff tab ******/ |
|
1807
|
{ |
|
1808
|
CX("<div id='fileedit-tab-diff' " |
|
1809
|
"data-tab-parent='fileedit-tabs' " |
|
1810
|
"data-tab-label='Diff' " |
|
1811
|
"class='hidden'" |
|
1812
|
">"); |
|
1813
|
|
|
1814
|
CX("<div class='fileedit-options flex-container " |
|
1815
|
"flex-row child-gap-small' " |
|
1816
|
"id='fileedit-tab-diff-buttons'>"); |
|
1817
|
CX("<button class='sbs'>Side-by-side</button>" |
|
1818
|
"<button class='unified'>Unified</button>"); |
|
1819
|
if(0){ |
|
1820
|
/* For the time being let's just ignore all whitespace |
|
1821
|
** changes, as files with Windows-style EOLs always show |
|
1822
|
** more diffs than we want then they're submitted to |
|
1823
|
** ?ajax=diff because JS normalizes them to Unix EOLs. |
|
1824
|
** We can revisit this decision later. */ |
|
1825
|
style_select_list_int("diff-ws-policy", |
|
1826
|
"diff_ws", "Whitespace", |
|
1827
|
"Whitespace handling policy.", |
|
1828
|
2, |
|
1829
|
"Diff all whitespace", 0, |
|
1830
|
"Ignore EOL whitespace", 1, |
|
1831
|
"Ignore all whitespace", 2, |
|
1832
|
NULL); |
|
1833
|
} |
|
1834
|
CX("</div>"); |
|
1835
|
CX("<div id='fileedit-tab-diff-wrapper'>" |
|
1836
|
"Diffs will be shown here." |
|
1837
|
"</div>"); |
|
1838
|
CX("</div>"/*#fileedit-tab-diff*/); |
|
1839
|
} |
|
1840
|
|
|
1841
|
/****** Commit ******/ |
|
1842
|
CX("<div id='fileedit-tab-commit' " |
|
1843
|
"data-tab-parent='fileedit-tabs' " |
|
1844
|
"data-tab-label='Commit' " |
|
1845
|
"class='hidden'" |
|
1846
|
">"); |
|
1847
|
{ |
|
1848
|
/******* Commit flags/options *******/ |
|
1849
|
CX("<div class='fileedit-options flex-container flex-row'>"); |
|
1850
|
style_labeled_checkbox("cb-dry-run", |
|
1851
|
"dry_run", "Dry-run?", "1", |
|
1852
|
0, |
|
1853
|
"In dry-run mode, the Commit button performs " |
|
1854
|
"all work needed for committing changes but " |
|
1855
|
"then rolls back the transaction, and thus " |
|
1856
|
"does not really commit."); |
|
1857
|
style_labeled_checkbox("cb-allow-fork", |
|
1858
|
"allow_fork", "Allow fork?", "1", |
|
1859
|
cimi.flags & CIMINI_ALLOW_FORK, |
|
1860
|
"Allow committing to create a fork?"); |
|
1861
|
style_labeled_checkbox("cb-allow-older", |
|
1862
|
"allow_older", "Allow older?", "1", |
|
1863
|
cimi.flags & CIMINI_ALLOW_OLDER, |
|
1864
|
"Allow saving against a parent version " |
|
1865
|
"which has a newer timestamp?"); |
|
1866
|
style_labeled_checkbox("cb-exec-bit", |
|
1867
|
"exec_bit", "Executable?", "1", |
|
1868
|
PERM_EXE==cimi.filePerm, |
|
1869
|
"Set the executable bit?"); |
|
1870
|
style_labeled_checkbox("cb-allow-merge-conflict", |
|
1871
|
"allow_merge_conflict", |
|
1872
|
"Allow merge conflict markers?", "1", |
|
1873
|
cimi.flags & CIMINI_ALLOW_MERGE_MARKER, |
|
1874
|
"Allow saving even if the content contains " |
|
1875
|
"what appear to be fossil merge conflict " |
|
1876
|
"markers?"); |
|
1877
|
style_labeled_checkbox("cb-prefer-delta", |
|
1878
|
"prefer_delta", |
|
1879
|
"Prefer delta manifest?", "1", |
|
1880
|
db_get_boolean("forbid-delta-manifests",0) |
|
1881
|
? 0 |
|
1882
|
: (db_get_boolean("seen-delta-manifest",0) |
|
1883
|
|| cimi.flags & CIMINI_PREFER_DELTA), |
|
1884
|
"Will create a delta manifest, instead of " |
|
1885
|
"baseline, if conditions are favorable to " |
|
1886
|
"do so. This option is only a suggestion."); |
|
1887
|
style_labeled_checkbox("cb-include-manifest", |
|
1888
|
"include_manifest", |
|
1889
|
"Response manifest?", "1", |
|
1890
|
0, |
|
1891
|
"Include the manifest in the response? " |
|
1892
|
"It's generally only useful for debug " |
|
1893
|
"purposes."); |
|
1894
|
style_select_list_int("select-eol-style", |
|
1895
|
"eol", "EOL Style", |
|
1896
|
"EOL conversion policy, noting that " |
|
1897
|
"webpage-side processing may implicitly change " |
|
1898
|
"the line endings of the input.", |
|
1899
|
(cimi.flags & CIMINI_CONVERT_EOL_UNIX) |
|
1900
|
? 1 : (cimi.flags & CIMINI_CONVERT_EOL_WINDOWS |
|
1901
|
? 2 : 0), |
|
1902
|
"Inherit", 0, |
|
1903
|
"Unix", 1, |
|
1904
|
"Windows", 2, |
|
1905
|
NULL); |
|
1906
|
|
|
1907
|
CX("</div>"/*checkboxes*/); |
|
1908
|
} |
|
1909
|
|
|
1910
|
{ /******* Commit comment, button, and result manifest *******/ |
|
1911
|
CX("<fieldset class='fileedit-options commit-message'>" |
|
1912
|
"<legend>Message (required)</legend><div>\n"); |
|
1913
|
/* We have two comment input fields, defaulting to single-line |
|
1914
|
** mode. JS code sets up the ability to toggle between single- |
|
1915
|
** and multi-line modes. */ |
|
1916
|
CX("<input type='text' name='comment' " |
|
1917
|
"id='fileedit-comment'></input>"); |
|
1918
|
CX("<textarea name='commentBig' class='hidden' " |
|
1919
|
"rows='5' id='fileedit-comment-big'></textarea>\n"); |
|
1920
|
{ /* comment options... */ |
|
1921
|
CX("<div class='flex-container flex-column child-gap-small'>"); |
|
1922
|
CX("<button id='comment-toggle' " |
|
1923
|
"title='Toggle between single- and multi-line comment mode, " |
|
1924
|
"noting that switching from multi- to single-line will cause " |
|
1925
|
"newlines to get stripped.'" |
|
1926
|
">Toggle single-/multi-line</button> "); |
|
1927
|
if(0){ |
|
1928
|
/* Manifests support an N-card (comment mime type) but it has |
|
1929
|
** yet to be honored where comments are rendered, so we don't |
|
1930
|
** currently offer it as an option here: |
|
1931
|
** https://fossil-scm.org/forum/forumpost/662da045a1 |
|
1932
|
** |
|
1933
|
** If/when it's ever implemented, simply enable this block and |
|
1934
|
** adjust the container's layout accordingly (as of this |
|
1935
|
** writing, that means changing the CSS class from |
|
1936
|
** 'flex-container flex-column' to 'flex-container flex-row'). |
|
1937
|
*/ |
|
1938
|
style_select_list_str("comment-mimetype", "comment_mimetype", |
|
1939
|
"Comment style:", |
|
1940
|
"Specify how fossil will interpret the " |
|
1941
|
"comment string.", |
|
1942
|
NULL, |
|
1943
|
"Fossil", "text/x-fossil-wiki", |
|
1944
|
"Markdown", "text/x-markdown", |
|
1945
|
"Plain text", "text/plain", |
|
1946
|
NULL); |
|
1947
|
CX("</div>\n"); |
|
1948
|
} |
|
1949
|
CX("<div class='fileedit-hint flex-container flex-row'>" |
|
1950
|
"(Warning: switching from multi- to single-line mode will " |
|
1951
|
"strip out all newlines!)</div>"); |
|
1952
|
} |
|
1953
|
CX("</div></fieldset>\n"/*commit comment options*/); |
|
1954
|
CX("<div class='flex-container flex-column' " |
|
1955
|
"id='fileedit-commit-button-wrapper'>" |
|
1956
|
"<button id='fileedit-btn-commit'>Commit</button>" |
|
1957
|
"</div>\n"); |
|
1958
|
CX("<div id='fileedit-manifest'></div>\n" |
|
1959
|
/* Manifest gets rendered here after a commit. */); |
|
1960
|
} |
|
1961
|
CX("</div>"/*#fileedit-tab-commit*/); |
|
1962
|
|
|
1963
|
/****** Help/Tips ******/ |
|
1964
|
CX("<div id='fileedit-tab-help' " |
|
1965
|
"data-tab-parent='fileedit-tabs' " |
|
1966
|
"data-tab-label='Help' " |
|
1967
|
"class='hidden'" |
|
1968
|
">"); |
|
1969
|
{ |
|
1970
|
CX("<h1>Help & Tips</h1>"); |
|
1971
|
CX("<ul>"); |
|
1972
|
CX("<li><strong>Only files matching the <code>fileedit-glob</code> " |
|
1973
|
"repository setting</strong> can be edited online. That setting " |
|
1974
|
"must be a comma- or newline-delimited list of glob patterns " |
|
1975
|
"for files which may be edited online.</li>"); |
|
1976
|
CX("<li>Committing edits creates a new commit record with a single " |
|
1977
|
"modified file.</li>"); |
|
1978
|
CX("<li>\"Delta manifests\" (see the checkbox on the Commit tab) " |
|
1979
|
"make for smaller commit records, especially in repositories " |
|
1980
|
"with many files.</li>"); |
|
1981
|
CX("<li>The file selector allows, for usability's sake, only files " |
|
1982
|
"in leaf check-ins to be selected, but files may be edited via " |
|
1983
|
"non-leaf check-ins by passing them as the <code>filename</code> " |
|
1984
|
"and <code>checkin</code> URL arguments to this page.</li>"); |
|
1985
|
CX("<li>The editor stores some number of local edits in one of " |
|
1986
|
"<code>window.fileStorage</code> or " |
|
1987
|
"<code>window.sessionStorage</code>, if able, but which storage " |
|
1988
|
"is unspecified and may differ across environments. When " |
|
1989
|
"committing or force-reloading a file, local edits to that " |
|
1990
|
"file/check-in combination are discarded.</li>"); |
|
1991
|
CX("</ul>"); |
|
1992
|
} |
|
1993
|
CX("</div>"/*#fileedit-tab-help*/); |
|
1994
|
|
|
1995
|
builtin_fossil_js_bundle_or("fetch", "dom", "tabs", "confirmer", |
|
1996
|
"storage", "popupwidget", "copybutton", |
|
1997
|
"pikchr", NULL); |
|
1998
|
/* |
|
1999
|
** Set up a JS-side mapping of the AJAX_RENDER_xyz values. This is |
|
2000
|
** used for dynamically toggling certain UI components on and off. |
|
2001
|
** Must come after window.fossil has been initialized and before |
|
2002
|
** fossil.page.fileedit.js. Potential TODO: move this into the |
|
2003
|
** window.fossil bootstrapping so that we don't have to "fulfill" |
|
2004
|
** the JS multiple times. |
|
2005
|
*/ |
|
2006
|
ajax_emit_js_preview_modes(1); |
|
2007
|
builtin_fossil_js_bundle_or("diff", NULL); |
|
2008
|
builtin_request_js("fossil.page.fileedit.js"); |
|
2009
|
builtin_fulfill_js_requests(); |
|
2010
|
{ |
|
2011
|
/* Dynamically populate the editor, display any error in the err |
|
2012
|
** blob, and/or switch to tab #0, where the file selector |
|
2013
|
** lives. The extra C scopes here correspond to JS-level scopes, |
|
2014
|
** to improve grokability. */ |
|
2015
|
style_script_begin(__FILE__,__LINE__); |
|
2016
|
CX("\n(function(){\n"); |
|
2017
|
CX("try{\n"); |
|
2018
|
{ |
|
2019
|
char * zFirstLeafUuid = 0; |
|
2020
|
CX("fossil.config['fileedit-glob'] = "); |
|
2021
|
glob_render_json_to_cgi(fileedit_glob()); |
|
2022
|
CX(";\n"); |
|
2023
|
if(blob_size(&err)>0){ |
|
2024
|
CX("fossil.error(%!j);\n", blob_str(&err)); |
|
2025
|
} |
|
2026
|
/* Populate the page with the current leaves and, if available, |
|
2027
|
the selected check-in's file list, to save 1 or 2 XHR requests |
|
2028
|
at startup. That makes this page uncacheable, but compressed |
|
2029
|
delivery of this page is currently less than 6k. */ |
|
2030
|
CX("fossil.page.initialLeaves = "); |
|
2031
|
fileedit_render_leaves_list(cimi.zParentUuid ? 0 : &zFirstLeafUuid); |
|
2032
|
CX(";\n"); |
|
2033
|
if(zFirstLeafUuid){ |
|
2034
|
assert(!cimi.zParentUuid); |
|
2035
|
cimi.zParentUuid = zFirstLeafUuid; |
|
2036
|
zFirstLeafUuid = 0; |
|
2037
|
} |
|
2038
|
if(cimi.zParentUuid){ |
|
2039
|
CX("fossil.page.initialFiles = "); |
|
2040
|
fileedit_render_checkin_files(cimi.zParentUuid); |
|
2041
|
CX(";\n"); |
|
2042
|
} |
|
2043
|
CX("fossil.onPageLoad(function(){\n"); |
|
2044
|
{ |
|
2045
|
if(blob_size(&err)>0){ |
|
2046
|
CX("fossil.error(%!j);\n", |
|
2047
|
blob_str(&err)); |
|
2048
|
CX("fossil.page.tabs.switchToTab(0);\n"); |
|
2049
|
} |
|
2050
|
if(cimi.zParentUuid && cimi.zFilename){ |
|
2051
|
CX("fossil.page.loadFile(%!j,%!j);\n", |
|
2052
|
cimi.zFilename, cimi.zParentUuid) |
|
2053
|
/* Reminder we cannot embed the JSON-format |
|
2054
|
content of the file here because if it contains |
|
2055
|
a SCRIPT tag then it will break the whole page. */; |
|
2056
|
} |
|
2057
|
} |
|
2058
|
CX("});\n")/*fossil.onPageLoad()*/; |
|
2059
|
} |
|
2060
|
CX("}catch(e){" |
|
2061
|
"fossil.error(e); console.error('Exception:',e);" |
|
2062
|
"}\n"); |
|
2063
|
CX("})();")/*anonymous function*/; |
|
2064
|
style_script_end(); |
|
2065
|
} |
|
2066
|
blob_reset(&err); |
|
2067
|
CheckinMiniInfo_cleanup(&cimi); |
|
2068
|
db_end_transaction(0); |
|
2069
|
style_finish_page(); |
|
2070
|
} |
|
2071
|
|