Fossil SCM

fossil-scm / src / fileedit.c
Blame History Raw 2071 lines
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 &amp; 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 &amp; 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

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button