VirtualBox

source: vbox/trunk/src/bldprogs/scmsubversion.cpp@ 107044

Last change on this file since 107044 was 106792, checked in by vboxsync, 3 weeks ago

scm: TortoiseSVN hack

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 59.9 KB
Line 
1/* $Id: scmsubversion.cpp 106792 2024-10-30 14:57:34Z vboxsync $ */
2/** @file
3 * IPRT Testcase / Tool - Source Code Massager, Subversion Access.
4 */
5
6/*
7 * Copyright (C) 2010-2024 Oracle and/or its affiliates.
8 *
9 * This file is part of VirtualBox base platform packages, as
10 * available from https://www.virtualbox.org.
11 *
12 * This program is free software; you can redistribute it and/or
13 * modify it under the terms of the GNU General Public License
14 * as published by the Free Software Foundation, in version 3 of the
15 * License.
16 *
17 * This program is distributed in the hope that it will be useful, but
18 * WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
20 * General Public License for more details.
21 *
22 * You should have received a copy of the GNU General Public License
23 * along with this program; if not, see <https://www.gnu.org/licenses>.
24 *
25 * SPDX-License-Identifier: GPL-3.0-only
26 */
27
28#define SCM_WITH_DYNAMIC_LIB_SVN
29
30
31/*********************************************************************************************************************************
32* Header Files *
33*********************************************************************************************************************************/
34#include <iprt/assert.h>
35#include <iprt/ctype.h>
36#include <iprt/dir.h>
37#include <iprt/env.h>
38#include <iprt/err.h>
39#include <iprt/file.h>
40#include <iprt/getopt.h>
41#include <iprt/handle.h>
42#include <iprt/initterm.h>
43#include <iprt/ldr.h>
44#include <iprt/mem.h>
45#include <iprt/message.h>
46#include <iprt/param.h>
47#include <iprt/path.h>
48#include <iprt/pipe.h>
49#include <iprt/poll.h>
50#include <iprt/process.h>
51#include <iprt/stream.h>
52#include <iprt/string.h>
53
54#include "scm.h"
55
56#if defined(SCM_WITH_DYNAMIC_LIB_SVN) && defined(SCM_WITH_SVN_HEADERS)
57# include <svn_client.h>
58#endif
59
60
61/*********************************************************************************************************************************
62* Defined Constants And Macros *
63*********************************************************************************************************************************/
64#ifdef SCM_WITH_DYNAMIC_LIB_SVN
65# if defined(RT_OS_WINDOWS) && defined(RT_ARCH_X86)
66# define APR_CALL __stdcall
67# define SVN_CALL /* __stdcall ?? */
68# else
69# define APR_CALL
70# define SVN_CALL
71# endif
72#endif
73#if defined(SCM_WITH_DYNAMIC_LIB_SVN) && !defined(SCM_WITH_SVN_HEADERS)
74# define SVN_ERR_MISC_CATEGORY_START 200000
75# define SVN_ERR_UNVERSIONED_RESOURCE (SVN_ERR_MISC_CATEGORY_START + 5)
76#endif
77
78
79/*********************************************************************************************************************************
80* Structures and Typedefs *
81*********************************************************************************************************************************/
82#if defined(SCM_WITH_DYNAMIC_LIB_SVN) && !defined(SCM_WITH_SVN_HEADERS)
83typedef int apr_status_t;
84typedef int64_t apr_time_t;
85typedef struct apr_pool_t apr_pool_t;
86typedef struct apr_hash_t apr_hash_t;
87typedef struct apr_hash_index_t apr_hash_index_t;
88typedef struct apr_array_header_t apr_array_header_t;
89
90
91typedef struct svn_error_t
92{
93 apr_status_t apr_err;
94 const char *_dbgr_message;
95 struct svn_error_t *_dbgr_child;
96 apr_pool_t *_dbgr_pool;
97 const char *_dbgr_file;
98 long _dbgr_line;
99} svn_error_t;
100typedef int svn_boolean_t;
101typedef long int svn_revnum_t;
102typedef struct svn_client_ctx_t svn_client_ctx_t;
103typedef enum svn_opt_revision_kind
104{
105 svn_opt_revision_unspecified = 0,
106 svn_opt_revision_number,
107 svn_opt_revision_date,
108 svn_opt_revision_committed,
109 svn_opt_revision_previous,
110 svn_opt_revision_base,
111 svn_opt_revision_working,
112 svn_opt_revision_head
113} svn_opt_revision_kind;
114typedef union svn_opt_revision_value_t
115{
116 svn_revnum_t number;
117 apr_time_t date;
118} svn_opt_revision_value_t;
119typedef struct svn_opt_revision_t
120{
121 svn_opt_revision_kind kind;
122 svn_opt_revision_value_t value;
123} svn_opt_revision_t;
124typedef enum svn_depth_t
125{
126 svn_depth_unknown = -2,
127 svn_depth_exclude,
128 svn_depth_empty,
129 svn_depth_files,
130 svn_depth_immediates,
131 svn_depth_infinity
132} svn_depth_t;
133
134#endif /* SCM_WITH_DYNAMIC_LIB_SVN && !SCM_WITH_SVN_HEADERS */
135
136
137/*********************************************************************************************************************************
138* Global Variables *
139*********************************************************************************************************************************/
140static char g_szSvnPath[RTPATH_MAX];
141static enum
142{
143 kScmSvnVersion_Ancient = 1,
144 kScmSvnVersion_1_6,
145 kScmSvnVersion_1_7,
146 kScmSvnVersion_1_8,
147 kScmSvnVersion_End
148} g_enmSvnVersion = kScmSvnVersion_Ancient;
149
150
151#ifdef SCM_WITH_DYNAMIC_LIB_SVN
152/** Set if all the function pointers are valid. */
153static bool g_fSvnFunctionPointersValid;
154/** @name SVN and APR imports.
155 * @{ */
156static apr_status_t (APR_CALL *g_pfnAprInitialize)(void);
157static apr_hash_index_t * (APR_CALL *g_pfnAprHashFirst)(apr_pool_t *pPool, apr_hash_t *pHashTab);
158static apr_hash_index_t * (APR_CALL *g_pfnAprHashNext)(apr_hash_index_t *pCurIdx);
159static void * (APR_CALL *g_pfnAprHashThisVal)(apr_hash_index_t *pHashIdx);
160static apr_pool_t * (SVN_CALL *g_pfnSvnPoolCreateEx)(apr_pool_t *pParent, void *pvAllocator);
161static void (APR_CALL *g_pfnAprPoolClear)(apr_pool_t *pPool);
162static void (APR_CALL *g_pfnAprPoolDestroy)(apr_pool_t *pPool);
163
164static svn_error_t * (SVN_CALL *g_pfnSvnClientCreateContext)(svn_client_ctx_t **ppCtx, apr_pool_t *pPool);
165static svn_error_t * (SVN_CALL *g_pfnSvnClientPropGet4)(apr_hash_t **ppHashProps, const char *pszPropName,
166 const char *pszTarget, const svn_opt_revision_t *pPeggedRev,
167 const svn_opt_revision_t *pRevision, svn_revnum_t *pActualRev,
168 svn_depth_t enmDepth, const apr_array_header_t *pChangeList,
169 svn_client_ctx_t *pCtx, apr_pool_t *pResultPool,
170 apr_pool_t *pScratchPool);
171/**@} */
172
173/** Cached APR pool. */
174static apr_pool_t *g_pSvnPool = NULL;
175/** Cached SVN client context. */
176static svn_client_ctx_t *g_pSvnClientCtx = NULL;
177/** Number of times the current context has been used. */
178static uint32_t g_cSvnClientCtxUsed = 0;
179
180#endif
181
182
183/*********************************************************************************************************************************
184* Internal Functions *
185*********************************************************************************************************************************/
186#ifdef SCM_WITH_DYNAMIC_LIB_SVN
187static void scmSvnFlushClientContextAndPool(void);
188#endif
189
190
191
192/**
193 * Callback that is call for each path to search.
194 */
195static DECLCALLBACK(int) scmSvnFindSvnBinaryCallback(char const *pchPath, size_t cchPath, void *pvUser1, void *pvUser2)
196{
197 char *pszDst = (char *)pvUser1;
198 size_t cchDst = (size_t)pvUser2;
199 if (cchDst > cchPath)
200 {
201 memcpy(pszDst, pchPath, cchPath);
202 pszDst[cchPath] = '\0';
203#if defined(RT_OS_WINDOWS) || defined(RT_OS_OS2)
204 int rc = RTPathAppend(pszDst, cchDst, "svn.exe");
205#else
206 int rc = RTPathAppend(pszDst, cchDst, "svn");
207#endif
208 if ( RT_SUCCESS(rc)
209 && RTFileExists(pszDst))
210 return VINF_SUCCESS;
211 }
212 return VERR_TRY_AGAIN;
213}
214
215
216/**
217 * Reads from a pipe.
218 *
219 * @returns @a rc or other status code.
220 * @param rc The current status of the operation. Error status
221 * are preserved and returned.
222 * @param phPipeR Pointer to the pipe handle.
223 * @param pcbAllocated Pointer to the buffer size variable.
224 * @param poffCur Pointer to the buffer offset variable.
225 * @param ppszBuffer Pointer to the buffer pointer variable.
226 */
227static int rtProcProcessOutput(int rc, PRTPIPE phPipeR, size_t *pcbAllocated, size_t *poffCur, char **ppszBuffer,
228 RTPOLLSET hPollSet, uint32_t idPollSet)
229{
230 size_t cbRead;
231 char szTmp[_4K - 1];
232 for (;;)
233 {
234 int rc2 = RTPipeRead(*phPipeR, szTmp, sizeof(szTmp), &cbRead);
235 if (RT_SUCCESS(rc2) && cbRead)
236 {
237 /* Resize the buffer. */
238 if (*poffCur + cbRead >= *pcbAllocated)
239 {
240 if (*pcbAllocated >= _1G)
241 {
242 RTPollSetRemove(hPollSet, idPollSet);
243 rc2 = RTPipeClose(*phPipeR); AssertRC(rc2);
244 *phPipeR = NIL_RTPIPE;
245 return RT_SUCCESS(rc) ? VERR_TOO_MUCH_DATA : rc;
246 }
247
248 size_t cbNew = *pcbAllocated ? *pcbAllocated * 2 : sizeof(szTmp) + 1;
249 Assert(*poffCur + cbRead < cbNew);
250 rc2 = RTStrRealloc(ppszBuffer, cbNew);
251 if (RT_FAILURE(rc2))
252 {
253 RTPollSetRemove(hPollSet, idPollSet);
254 rc2 = RTPipeClose(*phPipeR); AssertRC(rc2);
255 *phPipeR = NIL_RTPIPE;
256 return RT_SUCCESS(rc) ? rc2 : rc;
257 }
258 *pcbAllocated = cbNew;
259 }
260
261 /* Append the new data, terminating it. */
262 memcpy(*ppszBuffer + *poffCur, szTmp, cbRead);
263 *poffCur += cbRead;
264 (*ppszBuffer)[*poffCur] = '\0';
265
266 /* Check for null terminators in the string. */
267 if (RT_SUCCESS(rc) && memchr(szTmp, '\0', cbRead))
268 rc = VERR_NO_TRANSLATION;
269
270 /* If we read a full buffer, try read some more. */
271 if (RT_SUCCESS(rc) && cbRead == sizeof(szTmp))
272 continue;
273 }
274 else if (rc2 != VINF_TRY_AGAIN)
275 {
276 if (RT_FAILURE(rc) && rc2 != VERR_BROKEN_PIPE)
277 rc = rc2;
278 RTPollSetRemove(hPollSet, idPollSet);
279 rc2 = RTPipeClose(*phPipeR); AssertRC(rc2);
280 *phPipeR = NIL_RTPIPE;
281 }
282 return rc;
283 }
284}
285
286/** @name RTPROCEXEC_FLAGS_XXX - flags for RTProcExec and RTProcExecToString.
287 * @{ */
288/** Redirect /dev/null to standard input. */
289#define RTPROCEXEC_FLAGS_STDIN_NULL RT_BIT_32(0)
290/** Redirect standard output to /dev/null. */
291#define RTPROCEXEC_FLAGS_STDOUT_NULL RT_BIT_32(1)
292/** Redirect standard error to /dev/null. */
293#define RTPROCEXEC_FLAGS_STDERR_NULL RT_BIT_32(2)
294/** Redirect all standard output to /dev/null as well as directing /dev/null
295 * to standard input. */
296#define RTPROCEXEC_FLAGS_STD_NULL ( RTPROCEXEC_FLAGS_STDIN_NULL \
297 | RTPROCEXEC_FLAGS_STDOUT_NULL \
298 | RTPROCEXEC_FLAGS_STDERR_NULL)
299/** Mask containing the valid flags. */
300#define RTPROCEXEC_FLAGS_VALID_MASK UINT32_C(0x00000007)
301/** @} */
302
303/**
304 * Runs a process, collecting the standard output and/or standard error.
305 *
306 *
307 * @returns IPRT status code
308 * @retval VERR_NO_TRANSLATION if the output of the program isn't valid UTF-8
309 * or contains a nul character.
310 * @retval VERR_TOO_MUCH_DATA if the process produced too much data.
311 *
312 * @param pszExec Executable image to use to create the child process.
313 * @param papszArgs Pointer to an array of arguments to the child. The
314 * array terminated by an entry containing NULL.
315 * @param hEnv Handle to the environment block for the child.
316 * @param fFlags A combination of RTPROCEXEC_FLAGS_XXX. The @a
317 * ppszStdOut and @a ppszStdErr parameters takes precedence
318 * over redirection flags.
319 * @param pStatus Where to return the status on success.
320 * @param ppszStdOut Where to return the text written to standard output. If
321 * NULL then standard output will not be collected and go
322 * to the standard output handle of the process.
323 * Free with RTStrFree, regardless of return status.
324 * @param ppszStdErr Where to return the text written to standard error. If
325 * NULL then standard output will not be collected and go
326 * to the standard error handle of the process.
327 * Free with RTStrFree, regardless of return status.
328 */
329static int RTProcExecToString(const char *pszExec, const char * const *papszArgs, RTENV hEnv, uint32_t fFlags,
330 PRTPROCSTATUS pStatus, char **ppszStdOut, char **ppszStdErr)
331{
332 int rc2;
333
334 /*
335 * Clear output arguments (no returning failure here, simply crash!).
336 */
337 AssertPtr(pStatus);
338 pStatus->enmReason = RTPROCEXITREASON_ABEND;
339 pStatus->iStatus = RTEXITCODE_FAILURE;
340 AssertPtrNull(ppszStdOut);
341 if (ppszStdOut)
342 *ppszStdOut = NULL;
343 AssertPtrNull(ppszStdOut);
344 if (ppszStdErr)
345 *ppszStdErr = NULL;
346
347 /*
348 * Check input arguments.
349 */
350 AssertReturn(!(fFlags & ~RTPROCEXEC_FLAGS_VALID_MASK), VERR_INVALID_PARAMETER);
351
352 /*
353 * Do we need a standard input bitbucket?
354 */
355 int rc = VINF_SUCCESS;
356 PRTHANDLE phChildStdIn = NULL;
357 RTHANDLE hChildStdIn;
358 hChildStdIn.enmType = RTHANDLETYPE_FILE;
359 hChildStdIn.u.hFile = NIL_RTFILE;
360 if ((fFlags & RTPROCEXEC_FLAGS_STDIN_NULL) && RT_SUCCESS(rc))
361 {
362 phChildStdIn = &hChildStdIn;
363 rc = RTFileOpenBitBucket(&hChildStdIn.u.hFile, RTFILE_O_READ);
364 }
365
366 /*
367 * Create the output pipes / bitbuckets.
368 */
369 RTPIPE hPipeStdOutR = NIL_RTPIPE;
370 PRTHANDLE phChildStdOut = NULL;
371 RTHANDLE hChildStdOut;
372 hChildStdOut.enmType = RTHANDLETYPE_PIPE;
373 hChildStdOut.u.hPipe = NIL_RTPIPE;
374 if (ppszStdOut && RT_SUCCESS(rc))
375 {
376 phChildStdOut = &hChildStdOut;
377 rc = RTPipeCreate(&hPipeStdOutR, &hChildStdOut.u.hPipe, 0 /*fFlags*/);
378 }
379 else if ((fFlags & RTPROCEXEC_FLAGS_STDOUT_NULL) && RT_SUCCESS(rc))
380 {
381 phChildStdOut = &hChildStdOut;
382 hChildStdOut.enmType = RTHANDLETYPE_FILE;
383 hChildStdOut.u.hFile = NIL_RTFILE;
384 rc = RTFileOpenBitBucket(&hChildStdOut.u.hFile, RTFILE_O_WRITE);
385 }
386
387 RTPIPE hPipeStdErrR = NIL_RTPIPE;
388 PRTHANDLE phChildStdErr = NULL;
389 RTHANDLE hChildStdErr;
390 hChildStdErr.enmType = RTHANDLETYPE_PIPE;
391 hChildStdErr.u.hPipe = NIL_RTPIPE;
392 if (ppszStdErr && RT_SUCCESS(rc))
393 {
394 phChildStdErr = &hChildStdErr;
395 rc = RTPipeCreate(&hPipeStdErrR, &hChildStdErr.u.hPipe, 0 /*fFlags*/);
396 }
397 else if ((fFlags & RTPROCEXEC_FLAGS_STDERR_NULL) && RT_SUCCESS(rc))
398 {
399 phChildStdErr = &hChildStdErr;
400 hChildStdErr.enmType = RTHANDLETYPE_FILE;
401 hChildStdErr.u.hFile = NIL_RTFILE;
402 rc = RTFileOpenBitBucket(&hChildStdErr.u.hFile, RTFILE_O_WRITE);
403 }
404
405 if (RT_SUCCESS(rc))
406 {
407 RTPOLLSET hPollSet;
408 rc = RTPollSetCreate(&hPollSet);
409 if (RT_SUCCESS(rc))
410 {
411 if (hPipeStdOutR != NIL_RTPIPE && RT_SUCCESS(rc))
412 rc = RTPollSetAddPipe(hPollSet, hPipeStdOutR, RTPOLL_EVT_READ | RTPOLL_EVT_ERROR, 1);
413 if (hPipeStdErrR != NIL_RTPIPE)
414 rc = RTPollSetAddPipe(hPollSet, hPipeStdErrR, RTPOLL_EVT_READ | RTPOLL_EVT_ERROR, 2);
415 }
416 if (RT_SUCCESS(rc))
417 {
418 /*
419 * Create the process.
420 */
421 RTPROCESS hProc;
422 rc = RTProcCreateEx(pszExec,
423 papszArgs,
424 hEnv,
425 0 /*fFlags*/,
426 NULL /*phStdIn*/,
427 phChildStdOut,
428 phChildStdErr,
429 NULL /*pszAsUser*/,
430 NULL /*pszPassword*/,
431 NULL /*pvExtraData*/,
432 &hProc);
433 rc2 = RTHandleClose(&hChildStdErr); AssertRC(rc2);
434 rc2 = RTHandleClose(&hChildStdOut); AssertRC(rc2);
435
436 if (RT_SUCCESS(rc))
437 {
438 /*
439 * Process output and wait for the process to finish.
440 */
441 size_t cbStdOut = 0;
442 size_t offStdOut = 0;
443 size_t cbStdErr = 0;
444 size_t offStdErr = 0;
445 for (;;)
446 {
447 if (hPipeStdOutR != NIL_RTPIPE)
448 rc = rtProcProcessOutput(rc, &hPipeStdOutR, &cbStdOut, &offStdOut, ppszStdOut, hPollSet, 1);
449 if (hPipeStdErrR != NIL_RTPIPE)
450 rc = rtProcProcessOutput(rc, &hPipeStdErrR, &cbStdErr, &offStdErr, ppszStdErr, hPollSet, 2);
451 if (hPipeStdOutR == NIL_RTPIPE && hPipeStdErrR == NIL_RTPIPE)
452 break;
453
454 if (hProc != NIL_RTPROCESS)
455 {
456 rc2 = RTProcWait(hProc, RTPROCWAIT_FLAGS_NOBLOCK, pStatus);
457 if (rc2 != VERR_PROCESS_RUNNING)
458 {
459 if (RT_FAILURE(rc2))
460 rc = rc2;
461 hProc = NIL_RTPROCESS;
462 }
463 }
464
465 rc2 = RTPoll(hPollSet, 10000, NULL, NULL);
466 Assert(RT_SUCCESS(rc2) || rc2 == VERR_TIMEOUT);
467 }
468
469 if (RT_SUCCESS(rc))
470 {
471 if ( (ppszStdOut && *ppszStdOut && !RTStrIsValidEncoding(*ppszStdOut))
472 || (ppszStdErr && *ppszStdErr && !RTStrIsValidEncoding(*ppszStdErr)) )
473 rc = VERR_NO_TRANSLATION;
474 }
475
476 /*
477 * No more output, just wait for it to finish.
478 */
479 if (hProc != NIL_RTPROCESS)
480 {
481 rc2 = RTProcWait(hProc, RTPROCWAIT_FLAGS_BLOCK, pStatus);
482 if (RT_FAILURE(rc2))
483 rc = rc2;
484 }
485 }
486 RTPollSetDestroy(hPollSet);
487 }
488 }
489
490 rc2 = RTHandleClose(&hChildStdErr); AssertRC(rc2);
491 rc2 = RTHandleClose(&hChildStdOut); AssertRC(rc2);
492 rc2 = RTHandleClose(&hChildStdIn); AssertRC(rc2);
493 rc2 = RTPipeClose(hPipeStdErrR); AssertRC(rc2);
494 rc2 = RTPipeClose(hPipeStdOutR); AssertRC(rc2);
495 return rc;
496}
497
498
499/**
500 * Runs a process, waiting for it to complete.
501 *
502 * @returns IPRT status code
503 *
504 * @param pszExec Executable image to use to create the child process.
505 * @param papszArgs Pointer to an array of arguments to the child. The
506 * array terminated by an entry containing NULL.
507 * @param hEnv Handle to the environment block for the child.
508 * @param fFlags A combination of RTPROCEXEC_FLAGS_XXX.
509 * @param pStatus Where to return the status on success.
510 */
511static int RTProcExec(const char *pszExec, const char * const *papszArgs, RTENV hEnv, uint32_t fFlags,
512 PRTPROCSTATUS pStatus)
513{
514 int rc;
515
516 /*
517 * Clear output argument (no returning failure here, simply crash!).
518 */
519 AssertPtr(pStatus);
520 pStatus->enmReason = RTPROCEXITREASON_ABEND;
521 pStatus->iStatus = RTEXITCODE_FAILURE;
522
523 /*
524 * Check input arguments.
525 */
526 AssertReturn(!(fFlags & ~RTPROCEXEC_FLAGS_VALID_MASK), VERR_INVALID_PARAMETER);
527
528 /*
529 * Set up /dev/null redirections.
530 */
531 PRTHANDLE aph[3] = { NULL, NULL, NULL };
532 RTHANDLE ah[3];
533 for (uint32_t i = 0; i < 3; i++)
534 {
535 ah[i].enmType = RTHANDLETYPE_FILE;
536 ah[i].u.hFile = NIL_RTFILE;
537 }
538 rc = VINF_SUCCESS;
539 if ((fFlags & RTPROCEXEC_FLAGS_STDIN_NULL) && RT_SUCCESS(rc))
540 {
541 aph[0] = &ah[0];
542 rc = RTFileOpenBitBucket(&ah[0].u.hFile, RTFILE_O_READ);
543 }
544 if ((fFlags & RTPROCEXEC_FLAGS_STDOUT_NULL) && RT_SUCCESS(rc))
545 {
546 aph[1] = &ah[1];
547 rc = RTFileOpenBitBucket(&ah[1].u.hFile, RTFILE_O_WRITE);
548 }
549 if ((fFlags & RTPROCEXEC_FLAGS_STDERR_NULL) && RT_SUCCESS(rc))
550 {
551 aph[2] = &ah[2];
552 rc = RTFileOpenBitBucket(&ah[2].u.hFile, RTFILE_O_WRITE);
553 }
554
555 /*
556 * Create the process.
557 */
558 RTPROCESS hProc = NIL_RTPROCESS;
559 if (RT_SUCCESS(rc))
560 rc = RTProcCreateEx(pszExec,
561 papszArgs,
562 hEnv,
563 0 /*fFlags*/,
564 aph[0],
565 aph[1],
566 aph[2],
567 NULL /*pszAsUser*/,
568 NULL /*pszPassword*/,
569 NULL /*pvExtraData*/,
570 &hProc);
571
572 for (uint32_t i = 0; i < 3; i++)
573 RTFileClose(ah[i].u.hFile);
574
575 if (RT_SUCCESS(rc))
576 rc = RTProcWait(hProc, RTPROCWAIT_FLAGS_BLOCK, pStatus);
577 return rc;
578}
579
580
581
582/**
583 * Executes SVN and gets the output.
584 *
585 * Standard error is suppressed.
586 *
587 * @returns VINF_SUCCESS if the command executed successfully.
588 * @param pState The rewrite state to work on. Can be NULL.
589 * @param papszArgs The SVN argument.
590 * @param fNormalFailureOk Whether normal failure is ok.
591 * @param ppszStdOut Where to return the output on success.
592 */
593static int scmSvnRunAndGetOutput(PSCMRWSTATE pState, const char **papszArgs, bool fNormalFailureOk, char **ppszStdOut)
594{
595 *ppszStdOut = NULL;
596
597#ifdef SCM_WITH_DYNAMIC_LIB_SVN
598 scmSvnFlushClientContextAndPool();
599#endif
600
601 char *pszCmdLine = NULL;
602 int rc = RTGetOptArgvToString(&pszCmdLine, papszArgs, RTGETOPTARGV_CNV_QUOTE_BOURNE_SH);
603 if (RT_FAILURE(rc))
604 return rc;
605 ScmVerbose(pState, 2, "executing: %s\n", pszCmdLine);
606
607 RTPROCSTATUS Status;
608 rc = RTProcExecToString(g_szSvnPath, papszArgs, RTENV_DEFAULT,
609 RTPROCEXEC_FLAGS_STD_NULL, &Status, ppszStdOut, NULL);
610
611 if ( RT_SUCCESS(rc)
612 && ( Status.enmReason != RTPROCEXITREASON_NORMAL
613 || Status.iStatus != 0) )
614 {
615 if (fNormalFailureOk || Status.enmReason != RTPROCEXITREASON_NORMAL)
616 RTMsgError("%s: %s -> %s %u\n",
617 pState ? pState->pszFilename : "<NONE>", pszCmdLine,
618 Status.enmReason == RTPROCEXITREASON_NORMAL ? "exit code"
619 : Status.enmReason == RTPROCEXITREASON_SIGNAL ? "signal"
620 : Status.enmReason == RTPROCEXITREASON_ABEND ? "abnormal end"
621 : "abducted by alien",
622 Status.iStatus);
623 rc = VERR_GENERAL_FAILURE;
624 }
625 else if (RT_FAILURE(rc))
626 {
627 if (pState)
628 RTMsgError("%s: executing: %s => %Rrc\n", pState->pszFilename, pszCmdLine, rc);
629 else
630 RTMsgError("executing: %s => %Rrc\n", pszCmdLine, rc);
631 }
632
633 if (RT_FAILURE(rc))
634 {
635 RTStrFree(*ppszStdOut);
636 *ppszStdOut = NULL;
637 }
638 RTStrFree(pszCmdLine);
639 return rc;
640}
641
642
643/**
644 * Executes SVN.
645 *
646 * Standard error and standard output is suppressed.
647 *
648 * @returns VINF_SUCCESS if the command executed successfully.
649 * @param pState The rewrite state to work on.
650 * @param papszArgs The SVN argument.
651 * @param fNormalFailureOk Whether normal failure is ok.
652 */
653static int scmSvnRun(PSCMRWSTATE pState, const char **papszArgs, bool fNormalFailureOk)
654{
655#ifdef SCM_WITH_DYNAMIC_LIB_SVN
656 scmSvnFlushClientContextAndPool();
657#endif
658
659 char *pszCmdLine = NULL;
660 int rc = RTGetOptArgvToString(&pszCmdLine, papszArgs, RTGETOPTARGV_CNV_QUOTE_BOURNE_SH);
661 if (RT_FAILURE(rc))
662 return rc;
663 ScmVerbose(pState, 2, "executing: %s\n", pszCmdLine);
664
665 /* Lazy bird uses RTProcExec. */
666 RTPROCSTATUS Status;
667 rc = RTProcExec(g_szSvnPath, papszArgs, RTENV_DEFAULT, RTPROCEXEC_FLAGS_STD_NULL, &Status);
668
669 if ( RT_SUCCESS(rc)
670 && ( Status.enmReason != RTPROCEXITREASON_NORMAL
671 || Status.iStatus != 0) )
672 {
673 if (fNormalFailureOk || Status.enmReason != RTPROCEXITREASON_NORMAL)
674 RTMsgError("%s: %s -> %s %u\n",
675 pState->pszFilename,
676 pszCmdLine,
677 Status.enmReason == RTPROCEXITREASON_NORMAL ? "exit code"
678 : Status.enmReason == RTPROCEXITREASON_SIGNAL ? "signal"
679 : Status.enmReason == RTPROCEXITREASON_ABEND ? "abnormal end"
680 : "abducted by alien",
681 Status.iStatus);
682 rc = VERR_GENERAL_FAILURE;
683 }
684 else if (RT_FAILURE(rc))
685 RTMsgError("%s: %s -> %Rrc\n", pState->pszFilename, pszCmdLine, rc);
686
687 RTStrFree(pszCmdLine);
688 return rc;
689}
690
691
692#ifdef SCM_WITH_DYNAMIC_LIB_SVN
693/**
694 * Attempts to resolve the necessary subversion and apache portable runtime APIs
695 * we require dynamically.
696 *
697 * Will set all global function pointers and g_fSvnFunctionPointersValid to true
698 * on success.
699 */
700static void scmSvnTryResolveFunctions(void)
701{
702 char szPath[RTPATH_MAX];
703 int rc = RTStrCopy(szPath, sizeof(szPath), g_szSvnPath);
704 if (RT_SUCCESS(rc))
705 {
706 RTPathStripFilename(szPath);
707 char *pszEndPath = strchr(szPath, '\0');
708# ifdef RT_OS_WINDOWS
709 RTPathChangeToDosSlashes(szPath, false);
710# endif
711
712 /*
713 * Try various prefixes/suffxies/locations.
714 */
715 static struct
716 {
717 const char *pszPrefix;
718 const char *pszSuffix;
719 } const s_aVariations[] =
720 {
721# ifdef RT_OS_WINDOWS
722 { "SlikSvn-lib", "-1.dll" }, /* SlikSVN */
723 { "lib", "-1.dll" }, /* Win32Svn,CollabNet,++ */
724 { "lib", "_tsvn.dll" }, /* TortoiseSVN */
725# elif defined(RT_OS_DARWIN)
726 { "../lib/lib", "-1.dylib" },
727# else
728 { "../lib/lib", ".so" },
729 { "../lib/lib", "-1.so" },
730# if ARCH_BITS == 32
731 { "../lib32/lib", ".so" },
732 { "../lib32/lib", "-1.so" },
733# else
734 { "../lib64/lib", ".so" },
735 { "../lib64/lib", "-1.so" },
736# ifdef RT_OS_SOLARIS
737 { "../lib/svn/amd64/lib", ".so" },
738 { "../lib/svn/amd64/lib", "-1.so" },
739 { "../apr/1.6/lib/amd64/lib", ".so" },
740 { "../apr/1.6/lib/amd64/lib", "-1.so" },
741# endif
742# endif
743# ifdef RT_ARCH_X86
744 { "../lib/i386-linux-gnu/lib", ".so" },
745 { "../lib/i386-linux-gnu/lib", "-1.so" },
746# elif defined(RT_ARCH_AMD64)
747 { "../lib/x86_64-linux-gnu/lib", ".so" },
748 { "../lib/x86_64-linux-gnu/lib", "-1.so" },
749# endif
750# endif
751 };
752 for (unsigned iVar = 0; iVar < RT_ELEMENTS(s_aVariations); iVar++)
753 {
754 /*
755 * Try load the svn_client library ...
756 */
757 static const char * const s_apszLibraries[] = { "svn_client", "svn_subr", "apr" };
758 RTLDRMOD ahMods[RT_ELEMENTS(s_apszLibraries)] = { NIL_RTLDRMOD, NIL_RTLDRMOD, NIL_RTLDRMOD };
759
760 rc = VINF_SUCCESS;
761 unsigned iLib;
762 for (iLib = 0; iLib < RT_ELEMENTS(s_apszLibraries) && RT_SUCCESS(rc); iLib++)
763 {
764 static const char * const s_apszSuffixes[] = { "", ".0", ".1" };
765 for (unsigned iSuff = 0; iSuff < RT_ELEMENTS(s_apszSuffixes); iSuff++)
766 {
767 *pszEndPath = '\0';
768 rc = RTPathAppend(szPath, sizeof(szPath), s_aVariations[iVar].pszPrefix);
769 if (RT_SUCCESS(rc))
770 rc = RTStrCat(szPath, sizeof(szPath), s_apszLibraries[iLib]);
771 if (RT_SUCCESS(rc))
772 rc = RTStrCat(szPath, sizeof(szPath), s_aVariations[iVar].pszSuffix);
773 if (RT_SUCCESS(rc))
774 rc = RTStrCat(szPath, sizeof(szPath), s_apszSuffixes[iSuff]);
775 if (RT_SUCCESS(rc))
776 {
777# ifdef RT_OS_WINDOWS
778 RTPathChangeToDosSlashes(pszEndPath, false);
779# endif
780 rc = RTLdrLoadEx(szPath, &ahMods[iLib], RTLDRLOAD_FLAGS_NT_SEARCH_DLL_LOAD_DIR , NULL);
781# ifdef RT_OS_WINDOWS /* TortoiseSVN hack: */
782 if (RT_FAILURE(rc) && RTStrStartsWith(s_apszLibraries[iLib], "svn_"))
783 {
784 *pszEndPath = '\0';
785 rc = RTPathAppend(szPath, sizeof(szPath), s_aVariations[iVar].pszPrefix);
786 if (RT_SUCCESS(rc))
787 rc = RTStrCat(szPath, sizeof(szPath), "svn");
788 if (RT_SUCCESS(rc))
789 rc = RTStrCat(szPath, sizeof(szPath), s_aVariations[iVar].pszSuffix);
790 if (RT_SUCCESS(rc))
791 rc = RTStrCat(szPath, sizeof(szPath), s_apszSuffixes[iSuff]);
792 if (RT_SUCCESS(rc))
793 {
794 RTPathChangeToDosSlashes(pszEndPath, false);
795 rc = RTLdrLoadEx(szPath, &ahMods[iLib], RTLDRLOAD_FLAGS_NT_SEARCH_DLL_LOAD_DIR , NULL);
796 }
797 }
798#endif
799 if (RT_SUCCESS(rc))
800 {
801 RTMEM_WILL_LEAK(ahMods[iLib]);
802 break;
803 }
804 }
805 }
806# ifdef RT_OS_SOLARIS
807 /*
808 * HACK: Solaris may keep libapr.so separately from svn, so do a separate search for it.
809 */
810 /** @todo It would make a lot more sense to use the dlfcn.h machinery to figure
811 * out which libapr*.so* file was loaded into the process together with
812 * the two svn libraries and get a dlopen handle for it. We risk ending
813 * up with the completely wrong libapr here! */
814 if (iLib == RT_ELEMENTS(s_apszLibraries) - 1 && RT_FAILURE(rc))
815 {
816 ahMods[iLib] = NIL_RTLDRMOD;
817 for (unsigned iVar2 = 0; iVar2 < RT_ELEMENTS(s_aVariations) && ahMods[iLib] == NIL_RTLDRMOD; iVar2++)
818 for (unsigned iSuff2 = 0; iSuff2 < RT_ELEMENTS(s_apszSuffixes) && ahMods[iLib] == NIL_RTLDRMOD; iSuff2++)
819 {
820 *pszEndPath = '\0';
821 rc = RTPathAppend(szPath, sizeof(szPath), s_aVariations[iVar2].pszPrefix);
822 if (RT_SUCCESS(rc))
823 rc = RTStrCat(szPath, sizeof(szPath), s_apszLibraries[iLib]);
824 if (RT_SUCCESS(rc))
825 rc = RTStrCat(szPath, sizeof(szPath), s_aVariations[iVar2].pszSuffix);
826 if (RT_SUCCESS(rc))
827 rc = RTStrCat(szPath, sizeof(szPath), s_apszSuffixes[iSuff2]);
828 if (RT_SUCCESS(rc))
829 rc = RTLdrLoadEx(szPath, &ahMods[iLib], RTLDRLOAD_FLAGS_NT_SEARCH_DLL_LOAD_DIR, NULL);
830 if (RT_SUCCESS(rc))
831 RTMEM_WILL_LEAK(ahMods[iLib]);
832 else
833 ahMods[iLib] = NIL_RTLDRMOD;
834 }
835 }
836# endif /* RT_OS_SOLARIS */
837 }
838 if (iLib == RT_ELEMENTS(s_apszLibraries) && RT_SUCCESS(rc))
839 {
840 static const struct
841 {
842 unsigned iLib;
843 const char *pszSymbol;
844 uintptr_t *ppfn; /**< The nothrow attrib of PFNRT goes down the wrong way with Clang 11, thus uintptr_t. */
845 } s_aSymbols[] =
846 {
847 { 2, "apr_initialize", (uintptr_t *)&g_pfnAprInitialize },
848 { 2, "apr_hash_first", (uintptr_t *)&g_pfnAprHashFirst },
849 { 2, "apr_hash_next", (uintptr_t *)&g_pfnAprHashNext },
850 { 2, "apr_hash_this_val", (uintptr_t *)&g_pfnAprHashThisVal },
851 { 1, "svn_pool_create_ex", (uintptr_t *)&g_pfnSvnPoolCreateEx },
852 { 2, "apr_pool_clear", (uintptr_t *)&g_pfnAprPoolClear },
853 { 2, "apr_pool_destroy", (uintptr_t *)&g_pfnAprPoolDestroy },
854 { 0, "svn_client_create_context", (uintptr_t *)&g_pfnSvnClientCreateContext },
855 { 0, "svn_client_propget4", (uintptr_t *)&g_pfnSvnClientPropGet4 },
856 };
857 for (unsigned i = 0; i < RT_ELEMENTS(s_aSymbols); i++)
858 {
859 rc = RTLdrGetSymbol(ahMods[s_aSymbols[i].iLib], s_aSymbols[i].pszSymbol,
860 (void **)(uintptr_t)s_aSymbols[i].ppfn);
861 if (RT_FAILURE(rc))
862 {
863 ScmVerbose(NULL, 0, "Failed to resolve '%s' in '%s'",
864 s_aSymbols[i].pszSymbol, s_apszLibraries[s_aSymbols[i].iLib]);
865 break;
866 }
867 }
868 if (RT_SUCCESS(rc))
869 {
870 apr_status_t rcApr = g_pfnAprInitialize();
871 if (rcApr == 0)
872 {
873 ScmVerbose(NULL, 1, "Found subversion APIs.\n");
874 g_fSvnFunctionPointersValid = true;
875 }
876 else
877 {
878 ScmVerbose(NULL, 0, "apr_initialize failed: %#x (%d)\n", rcApr, rcApr);
879 AssertMsgFailed(("%#x (%d)\n", rc, rc));
880 }
881 return;
882 }
883 }
884
885 while (iLib-- > 0)
886 RTLdrClose(ahMods[iLib]);
887 }
888 }
889}
890#endif /* SCM_WITH_DYNAMIC_LIB_SVN */
891
892
893/**
894 * Finds the svn binary, updating g_szSvnPath and g_enmSvnVersion.
895 */
896static void scmSvnFindSvnBinary(PSCMRWSTATE pState)
897{
898 /* Already been called? */
899 if (g_szSvnPath[0] != '\0')
900 return;
901
902 /*
903 * Locate it.
904 */
905 /** @todo code page fun... */
906#ifdef RT_OS_WINDOWS
907 const char *pszEnvVar = RTEnvGet("Path");
908#else
909 const char *pszEnvVar = RTEnvGet("PATH");
910#endif
911 if (pszEnvVar)
912 {
913#if defined(RT_OS_WINDOWS) || defined(RT_OS_OS2)
914 int rc = RTPathTraverseList(pszEnvVar, ';', scmSvnFindSvnBinaryCallback, g_szSvnPath, (void *)sizeof(g_szSvnPath));
915#else
916 int rc = RTPathTraverseList(pszEnvVar, ':', scmSvnFindSvnBinaryCallback, g_szSvnPath, (void *)sizeof(g_szSvnPath));
917#endif
918 if (RT_FAILURE(rc))
919 strcpy(g_szSvnPath, "svn");
920 }
921 else
922 strcpy(g_szSvnPath, "svn");
923
924 /*
925 * Check the version.
926 */
927 const char *apszArgs[] = { g_szSvnPath, "--version", "--quiet", NULL };
928 char *pszVersion;
929 int rc = scmSvnRunAndGetOutput(pState, apszArgs, false, &pszVersion);
930 if (RT_SUCCESS(rc))
931 {
932 char *pszStripped = RTStrStrip(pszVersion);
933 if (RTStrVersionCompare(pszStripped, "1.8") >= 0)
934 g_enmSvnVersion = kScmSvnVersion_1_8;
935 else if (RTStrVersionCompare(pszStripped, "1.7") >= 0)
936 g_enmSvnVersion = kScmSvnVersion_1_7;
937 else if (RTStrVersionCompare(pszStripped, "1.6") >= 0)
938 g_enmSvnVersion = kScmSvnVersion_1_6;
939 else
940 g_enmSvnVersion = kScmSvnVersion_Ancient;
941 RTStrFree(pszVersion);
942 }
943 else
944 g_enmSvnVersion = kScmSvnVersion_Ancient;
945
946#ifdef SCM_WITH_DYNAMIC_LIB_SVN
947 /*
948 * If we got version 1.8 or later, try see if we can locate a few of the
949 * simpler SVN APIs.
950 */
951 g_fSvnFunctionPointersValid = false;
952 if (g_enmSvnVersion >= kScmSvnVersion_1_8)
953 scmSvnTryResolveFunctions();
954#endif
955}
956
957
958/**
959 * Construct a dot svn filename for the file being rewritten.
960 *
961 * @returns IPRT status code.
962 * @param pState The rewrite state (for the name).
963 * @param pszDir The directory, including ".svn/".
964 * @param pszSuff The filename suffix.
965 * @param pszDst The output buffer. RTPATH_MAX in size.
966 */
967static int scmSvnConstructName(PSCMRWSTATE pState, const char *pszDir, const char *pszSuff, char *pszDst)
968{
969 strcpy(pszDst, pState->pszFilename); /* ASSUMES sizeof(szBuf) <= sizeof(szPath) */
970 RTPathStripFilename(pszDst);
971
972 int rc = RTPathAppend(pszDst, RTPATH_MAX, pszDir);
973 if (RT_SUCCESS(rc))
974 {
975 rc = RTPathAppend(pszDst, RTPATH_MAX, RTPathFilename(pState->pszFilename));
976 if (RT_SUCCESS(rc))
977 {
978 size_t cchDst = strlen(pszDst);
979 size_t cchSuff = strlen(pszSuff);
980 if (cchDst + cchSuff < RTPATH_MAX)
981 {
982 memcpy(&pszDst[cchDst], pszSuff, cchSuff + 1);
983 return VINF_SUCCESS;
984 }
985 else
986 rc = VERR_BUFFER_OVERFLOW;
987 }
988 }
989 return rc;
990}
991
992/**
993 * Interprets the specified string as decimal numbers.
994 *
995 * @returns true if parsed successfully, false if not.
996 * @param pch The string (not terminated).
997 * @param cch The string length.
998 * @param pu Where to return the value.
999 */
1000static bool scmSvnReadNumber(const char *pch, size_t cch, size_t *pu)
1001{
1002 size_t u = 0;
1003 while (cch-- > 0)
1004 {
1005 char ch = *pch++;
1006 if (ch < '0' || ch > '9')
1007 return false;
1008 u *= 10;
1009 u += ch - '0';
1010 }
1011 *pu = u;
1012 return true;
1013}
1014
1015
1016#ifdef SCM_WITH_DYNAMIC_LIB_SVN
1017
1018/**
1019 * Wrapper around RTPathAbs.
1020 * @returns Same as RTPathAbs.
1021 * @param pszPath The relative path.
1022 * @param pszAbsPath Where to return the absolute path.
1023 * @param cbAbsPath Size of the @a pszAbsPath buffer.
1024 */
1025static int scmSvnAbsPath(const char *pszPath, char *pszAbsPath, size_t cbAbsPath)
1026{
1027 int rc = RTPathAbs(pszPath, pszAbsPath, cbAbsPath);
1028# if RTPATH_STYLE == RTPATH_STR_F_STYLE_DOS
1029 if (RT_SUCCESS(rc))
1030 {
1031 RTPathChangeToUnixSlashes(pszAbsPath, true /*fForce*/);
1032 /* To avoid: svn: E235000: In file '..\..\..\subversion\libsvn_client\prop_commands.c' line 796: assertion failed (svn_dirent_is_absolute(target)) */
1033 if (pszAbsPath[1] == ':')
1034 pszAbsPath[0] = RT_C_TO_UPPER(pszAbsPath[0]);
1035 }
1036# endif
1037 return rc;
1038}
1039
1040
1041/**
1042 * Gets a client context and pool.
1043 *
1044 * This implements caching.
1045 *
1046 * @returns IPRT status code.
1047 * @param ppCtx Where to return the context
1048 * @param ppPool Where to return the pool.
1049 */
1050static int scmSvnGetClientContextAndPool(svn_client_ctx_t **ppCtx, apr_pool_t **ppPool)
1051{
1052 /*
1053 * Use cached if present.
1054 */
1055 if (g_pSvnClientCtx && g_pSvnPool)
1056 {
1057 g_cSvnClientCtxUsed++;
1058 *ppCtx = g_pSvnClientCtx;
1059 *ppPool = g_pSvnPool;
1060 return VINF_SUCCESS;
1061 }
1062 Assert(!g_pSvnClientCtx);
1063 Assert(!g_pSvnPool);
1064
1065 /*
1066 * Create new pool and context.
1067 */
1068 apr_pool_t *pPool = g_pfnSvnPoolCreateEx(NULL, NULL);
1069 if (pPool)
1070 {
1071 svn_client_ctx_t *pCtx = NULL;
1072 svn_error_t *pErr = g_pfnSvnClientCreateContext(&pCtx, pPool);
1073 if (!pErr)
1074 {
1075 g_cSvnClientCtxUsed = 1;
1076 g_pSvnClientCtx = *ppCtx = pCtx;
1077 g_pSvnPool = *ppPool = pPool;
1078 return VINF_SUCCESS;
1079 }
1080 g_pfnAprPoolDestroy(pPool);
1081 }
1082
1083 *ppCtx = NULL;
1084 *ppPool = NULL;
1085 return VERR_GENERAL_FAILURE;
1086}
1087
1088
1089/**
1090 * Puts back a client context and pool after use.
1091 *
1092 * @param pCtx The context.
1093 * @param pPool The pool.
1094 * @param fFlush Whether to flush it.
1095 */
1096static void scmSvnPutClientContextAndPool(svn_client_ctx_t *pCtx, apr_pool_t *pPool, bool fFlush)
1097{
1098 if (fFlush || g_cSvnClientCtxUsed > 4096) /* Disable this to force new context every time. */
1099 {
1100 g_pfnAprPoolDestroy(pPool);
1101 g_pSvnPool = NULL;
1102 g_pSvnClientCtx = NULL;
1103 }
1104 RT_NOREF(pCtx, fFlush);
1105}
1106
1107
1108/**
1109 * Flushes the cached client context and pool
1110 */
1111static void scmSvnFlushClientContextAndPool(void)
1112{
1113 if (g_pSvnPool)
1114 scmSvnPutClientContextAndPool(g_pSvnClientCtx, g_pSvnPool, true /*fFlush*/);
1115 Assert(!g_pSvnPool);
1116}
1117
1118
1119/**
1120 * Checks if @a pszPath exists in the current WC.
1121 *
1122 * @returns true, false or -1. In the latter case, please use the fallback.
1123 * @param pszPath Path to the object that should be investigated.
1124 */
1125static int scmSvnIsObjectInWorkingCopy(const char *pszPath)
1126{
1127 /* svn_client_propget4 and later requires absolute target path. */
1128 char szAbsPath[RTPATH_MAX];
1129 int rc = scmSvnAbsPath(pszPath, szAbsPath, sizeof(szAbsPath));
1130 if (RT_SUCCESS(rc))
1131 {
1132 apr_pool_t *pPool;
1133 svn_client_ctx_t *pCtx = NULL;
1134 rc = scmSvnGetClientContextAndPool(&pCtx, &pPool);
1135 if (RT_SUCCESS(rc))
1136 {
1137 /* Make the call. */
1138 apr_hash_t *pHash = NULL;
1139 svn_opt_revision_t Rev;
1140 RT_ZERO(Rev);
1141 Rev.kind = svn_opt_revision_working;
1142 Rev.value.number = -1L;
1143 svn_error_t *pErr = g_pfnSvnClientPropGet4(&pHash, "svn:no-such-property", szAbsPath, &Rev, &Rev,
1144 NULL /*pActualRev*/, svn_depth_empty, NULL /*pChangeList*/,
1145 pCtx, pPool, pPool);
1146 if (!pErr)
1147 rc = true;
1148 else if (pErr->apr_err == SVN_ERR_UNVERSIONED_RESOURCE)
1149 rc = false;
1150
1151 scmSvnPutClientContextAndPool(pCtx, pPool, false);
1152 }
1153 }
1154 return rc;
1155}
1156
1157#endif /* SCM_WITH_DYNAMIC_LIB_SVN */
1158
1159
1160/**
1161 * Checks if the file we're operating on is part of a SVN working copy.
1162 *
1163 * @returns true if it is, false if it isn't or we cannot tell.
1164 * @param pState The rewrite state to work on. Will use the
1165 * fIsInSvnWorkingCopy member for caching the result.
1166 */
1167bool ScmSvnIsInWorkingCopy(PSCMRWSTATE pState)
1168{
1169 /*
1170 * We don't ask SVN twice as that's expensive.
1171 */
1172 if (pState->fIsInSvnWorkingCopy != 0)
1173 return pState->fIsInSvnWorkingCopy > 0;
1174
1175#ifdef SCM_WITH_DYNAMIC_LIB_SVN
1176 if (g_fSvnFunctionPointersValid)
1177 {
1178 int rc = scmSvnIsObjectInWorkingCopy(pState->pszFilename);
1179 if (rc == (int)true || rc == (int)false)
1180 {
1181 pState->fIsInSvnWorkingCopy = rc == (int)true ? 1 : -1;
1182 return rc == (int)true;
1183 }
1184 }
1185
1186 /* Fallback: */
1187#endif
1188 if (g_enmSvnVersion < kScmSvnVersion_1_7)
1189 {
1190 /*
1191 * Hack: check if the .svn/text-base/<file>.svn-base file exists.
1192 */
1193 char szPath[RTPATH_MAX];
1194 int rc = scmSvnConstructName(pState, ".svn/text-base/", ".svn-base", szPath);
1195 if (RT_SUCCESS(rc))
1196 {
1197 if (RTFileExists(szPath))
1198 {
1199 pState->fIsInSvnWorkingCopy = 1;
1200 return true;
1201 }
1202 }
1203 }
1204 else
1205 {
1206 const char *apszArgs[] = { g_szSvnPath, "proplist", pState->pszFilename, NULL };
1207 char *pszValue;
1208 int rc = scmSvnRunAndGetOutput(pState, apszArgs, true, &pszValue);
1209 if (RT_SUCCESS(rc))
1210 {
1211 RTStrFree(pszValue);
1212 pState->fIsInSvnWorkingCopy = 1;
1213 return true;
1214 }
1215 }
1216 pState->fIsInSvnWorkingCopy = -1;
1217 return false;
1218}
1219
1220
1221/**
1222 * Checks if the specified directory is part of a SVN working copy.
1223 *
1224 * @returns true if it is, false if it isn't or we cannot tell.
1225 * @param pszDir The directory in question.
1226 */
1227bool ScmSvnIsDirInWorkingCopy(const char *pszDir)
1228{
1229#ifdef SCM_WITH_DYNAMIC_LIB_SVN
1230 if (g_fSvnFunctionPointersValid)
1231 {
1232 int rc = scmSvnIsObjectInWorkingCopy(pszDir);
1233 if (rc == (int)true || rc == (int)false)
1234 return rc == (int)true;
1235 }
1236
1237 /* Fallback: */
1238#endif
1239 if (g_enmSvnVersion < kScmSvnVersion_1_7)
1240 {
1241 /*
1242 * Hack: check if the .svn/ dir exists.
1243 */
1244 char szPath[RTPATH_MAX];
1245 int rc = RTPathJoin(szPath, sizeof(szPath), pszDir, ".svn");
1246 if (RT_SUCCESS(rc))
1247 return RTDirExists(szPath);
1248 }
1249 else
1250 {
1251 const char *apszArgs[] = { g_szSvnPath, "propget", "svn:no-such-property", pszDir, NULL };
1252 char *pszValue;
1253 int rc = scmSvnRunAndGetOutput(NULL, apszArgs, true, &pszValue);
1254 if (RT_SUCCESS(rc))
1255 {
1256 RTStrFree(pszValue);
1257 return true;
1258 }
1259 }
1260 return false;
1261}
1262
1263
1264#ifdef SCM_WITH_DYNAMIC_LIB_SVN
1265/**
1266 * Checks if @a pszPath exists in the current WC.
1267 *
1268 * @returns IPRT status code - VERR_NOT_SUPPORT if fallback should be attempted.
1269 * @param pszPath Path to the object that should be investigated.
1270 * @param pszProperty The property name.
1271 * @param ppszValue Where to return the property value. Optional.
1272 */
1273static int scmSvnQueryPropertyUsingApi(const char *pszPath, const char *pszProperty, char **ppszValue)
1274{
1275 /* svn_client_propget4 and later requires absolute target path. */
1276 char szAbsPath[RTPATH_MAX];
1277 int rc = scmSvnAbsPath(pszPath, szAbsPath, sizeof(szAbsPath));
1278 if (RT_SUCCESS(rc))
1279 {
1280 apr_pool_t *pPool;
1281 svn_client_ctx_t *pCtx = NULL;
1282 rc = scmSvnGetClientContextAndPool(&pCtx, &pPool);
1283 if (RT_SUCCESS(rc))
1284 {
1285 /* Make the call. */
1286 apr_hash_t *pHash = NULL;
1287 svn_opt_revision_t Rev;
1288 RT_ZERO(Rev);
1289 Rev.kind = svn_opt_revision_working;
1290 Rev.value.number = -1L;
1291 svn_error_t *pErr = g_pfnSvnClientPropGet4(&pHash, pszProperty, szAbsPath, &Rev, &Rev,
1292 NULL /*pActualRev*/, svn_depth_empty, NULL /*pChangeList*/,
1293 pCtx, pPool, pPool);
1294 if (!pErr)
1295 {
1296 /* Get the first value, if any. */
1297 rc = VERR_NOT_FOUND;
1298 apr_hash_index_t *pHashIdx = g_pfnAprHashFirst(pPool, pHash);
1299 if (pHashIdx)
1300 {
1301 const char **ppszFirst = (const char **)g_pfnAprHashThisVal(pHashIdx);
1302 if (ppszFirst && *ppszFirst)
1303 {
1304 if (ppszValue)
1305 rc = RTStrDupEx(ppszValue, *ppszFirst);
1306 else
1307 rc = VINF_SUCCESS;
1308 }
1309 }
1310 }
1311 else if (pErr->apr_err == SVN_ERR_UNVERSIONED_RESOURCE)
1312 rc = VERR_INVALID_STATE;
1313 else
1314 rc = VERR_GENERAL_FAILURE;
1315
1316 scmSvnPutClientContextAndPool(pCtx, pPool, false);
1317 }
1318 }
1319 return rc;
1320}
1321#endif /* SCM_WITH_DYNAMIC_LIB_SVN */
1322
1323
1324/**
1325 * Queries the value of an SVN property.
1326 *
1327 * This will automatically adjust for scheduled changes.
1328 *
1329 * @returns IPRT status code.
1330 * @retval VERR_INVALID_STATE if not a SVN WC file.
1331 * @retval VERR_NOT_FOUND if the property wasn't found.
1332 * @param pState The rewrite state to work on.
1333 * @param pszName The property name.
1334 * @param ppszValue Where to return the property value. Free this
1335 * using RTStrFree. Optional.
1336 */
1337int ScmSvnQueryProperty(PSCMRWSTATE pState, const char *pszName, char **ppszValue)
1338{
1339 int rc;
1340
1341 /*
1342 * Look it up in the scheduled changes.
1343 */
1344 size_t i = pState->cSvnPropChanges;
1345 while (i-- > 0)
1346 if (!strcmp(pState->paSvnPropChanges[i].pszName, pszName))
1347 {
1348 const char *pszValue = pState->paSvnPropChanges[i].pszValue;
1349 if (!pszValue)
1350 return VERR_NOT_FOUND;
1351 if (ppszValue)
1352 return RTStrDupEx(ppszValue, pszValue);
1353 return VINF_SUCCESS;
1354 }
1355
1356#ifdef SCM_WITH_DYNAMIC_LIB_SVN
1357 if (g_fSvnFunctionPointersValid)
1358 {
1359 rc = scmSvnQueryPropertyUsingApi(pState->pszFilename, pszName, ppszValue);
1360 if (rc != VERR_NOT_SUPPORTED)
1361 return rc;
1362 /* Fallback: */
1363 }
1364#endif
1365
1366 if (g_enmSvnVersion < kScmSvnVersion_1_7)
1367 {
1368 /*
1369 * Hack: Read the .svn/props/<file>.svn-work file exists.
1370 */
1371 char szPath[RTPATH_MAX];
1372 rc = scmSvnConstructName(pState, ".svn/props/", ".svn-work", szPath);
1373 if (RT_SUCCESS(rc) && !RTFileExists(szPath))
1374 rc = scmSvnConstructName(pState, ".svn/prop-base/", ".svn-base", szPath);
1375 if (RT_SUCCESS(rc))
1376 {
1377 SCMSTREAM Stream;
1378 rc = ScmStreamInitForReading(&Stream, szPath);
1379 if (RT_SUCCESS(rc))
1380 {
1381 /*
1382 * The current format is K len\n<name>\nV len\n<value>\n" ... END.
1383 */
1384 rc = VERR_NOT_FOUND;
1385 size_t const cchName = strlen(pszName);
1386 SCMEOL enmEol;
1387 size_t cchLine;
1388 const char *pchLine;
1389 while ((pchLine = ScmStreamGetLine(&Stream, &cchLine, &enmEol)) != NULL)
1390 {
1391 /*
1392 * Parse the 'K num' / 'END' line.
1393 */
1394 if ( cchLine == 3
1395 && !memcmp(pchLine, "END", 3))
1396 break;
1397 size_t cchKey;
1398 if ( cchLine < 3
1399 || pchLine[0] != 'K'
1400 || pchLine[1] != ' '
1401 || !scmSvnReadNumber(&pchLine[2], cchLine - 2, &cchKey)
1402 || cchKey == 0
1403 || cchKey > 4096)
1404 {
1405 RTMsgError("%s:%u: Unexpected data '%.*s'\n", szPath, ScmStreamTellLine(&Stream), cchLine, pchLine);
1406 rc = VERR_PARSE_ERROR;
1407 break;
1408 }
1409
1410 /*
1411 * Match the key and skip to the value line. Don't bother with
1412 * names containing EOL markers.
1413 */
1414 size_t const offKey = ScmStreamTell(&Stream);
1415 bool fMatch = cchName == cchKey;
1416 if (fMatch)
1417 {
1418 pchLine = ScmStreamGetLine(&Stream, &cchLine, &enmEol);
1419 if (!pchLine)
1420 break;
1421 fMatch = cchLine == cchName
1422 && !memcmp(pchLine, pszName, cchName);
1423 }
1424
1425 if (RT_FAILURE(ScmStreamSeekAbsolute(&Stream, offKey + cchKey)))
1426 break;
1427 if (RT_FAILURE(ScmStreamSeekByLine(&Stream, ScmStreamTellLine(&Stream) + 1)))
1428 break;
1429
1430 /*
1431 * Read and Parse the 'V num' line.
1432 */
1433 pchLine = ScmStreamGetLine(&Stream, &cchLine, &enmEol);
1434 if (!pchLine)
1435 break;
1436 size_t cchValue;
1437 if ( cchLine < 3
1438 || pchLine[0] != 'V'
1439 || pchLine[1] != ' '
1440 || !scmSvnReadNumber(&pchLine[2], cchLine - 2, &cchValue)
1441 || cchValue > _1M)
1442 {
1443 RTMsgError("%s:%u: Unexpected data '%.*s'\n", szPath, ScmStreamTellLine(&Stream), cchLine, pchLine);
1444 rc = VERR_PARSE_ERROR;
1445 break;
1446 }
1447
1448 /*
1449 * If we have a match, allocate a return buffer and read the
1450 * value into it. Otherwise skip this value and continue
1451 * searching.
1452 */
1453 if (fMatch)
1454 {
1455 if (!ppszValue)
1456 rc = VINF_SUCCESS;
1457 else
1458 {
1459 char *pszValue;
1460 rc = RTStrAllocEx(&pszValue, cchValue + 1);
1461 if (RT_SUCCESS(rc))
1462 {
1463 rc = ScmStreamRead(&Stream, pszValue, cchValue);
1464 if (RT_SUCCESS(rc))
1465 *ppszValue = pszValue;
1466 else
1467 RTStrFree(pszValue);
1468 }
1469 }
1470 break;
1471 }
1472
1473 if (RT_FAILURE(ScmStreamSeekRelative(&Stream, cchValue)))
1474 break;
1475 if (RT_FAILURE(ScmStreamSeekByLine(&Stream, ScmStreamTellLine(&Stream) + 1)))
1476 break;
1477 }
1478
1479 if (RT_FAILURE(ScmStreamGetStatus(&Stream)))
1480 {
1481 rc = ScmStreamGetStatus(&Stream);
1482 RTMsgError("%s: stream error %Rrc\n", szPath, rc);
1483 }
1484 ScmStreamDelete(&Stream);
1485 }
1486 }
1487
1488 if (rc == VERR_FILE_NOT_FOUND)
1489 rc = VERR_NOT_FOUND;
1490 }
1491 else
1492 {
1493 const char *apszArgs[] = { g_szSvnPath, "propget", "--strict", pszName, pState->pszFilename, NULL };
1494 char *pszValue;
1495 rc = scmSvnRunAndGetOutput(pState, apszArgs, false, &pszValue);
1496 if (RT_SUCCESS(rc))
1497 {
1498 if (pszValue && *pszValue)
1499 {
1500 if (ppszValue)
1501 {
1502 *ppszValue = pszValue;
1503 pszValue = NULL;
1504 }
1505 }
1506 else
1507 rc = VERR_NOT_FOUND;
1508 RTStrFree(pszValue);
1509 }
1510 }
1511 return rc;
1512}
1513
1514
1515/**
1516 * Queries the value of an SVN property on the parent dir/whatever.
1517 *
1518 * This will not adjust for scheduled changes to the parent!
1519 *
1520 * @returns IPRT status code.
1521 * @retval VERR_INVALID_STATE if not a SVN WC file.
1522 * @retval VERR_NOT_FOUND if the property wasn't found.
1523 * @param pState The rewrite state to work on.
1524 * @param pszName The property name.
1525 * @param ppszValue Where to return the property value. Free this
1526 * using RTStrFree. Optional.
1527 */
1528int ScmSvnQueryParentProperty(PSCMRWSTATE pState, const char *pszName, char **ppszValue)
1529{
1530 /*
1531 * Strip the filename and use ScmSvnQueryProperty.
1532 */
1533 char szPath[RTPATH_MAX];
1534 int rc = RTStrCopy(szPath, sizeof(szPath), pState->pszFilename);
1535 if (RT_SUCCESS(rc))
1536 {
1537 RTPathStripFilename(szPath);
1538 SCMRWSTATE ParentState;
1539 ParentState.pszFilename = szPath;
1540 ParentState.fFirst = false;
1541 ParentState.fNeedsManualRepair = false;
1542 ParentState.fIsInSvnWorkingCopy = true;
1543 ParentState.cSvnPropChanges = 0;
1544 ParentState.paSvnPropChanges = NULL;
1545 ParentState.rc = VINF_SUCCESS;
1546 rc = ScmSvnQueryProperty(&ParentState, pszName, ppszValue);
1547 if (RT_SUCCESS(rc))
1548 rc = ParentState.rc;
1549 }
1550 return rc;
1551}
1552
1553
1554/**
1555 * Schedules the setting of a property.
1556 *
1557 * @returns IPRT status code.
1558 * @retval VERR_INVALID_STATE if not a SVN WC file.
1559 * @param pState The rewrite state to work on.
1560 * @param pszName The name of the property to set.
1561 * @param pszValue The value. NULL means deleting it.
1562 */
1563int ScmSvnSetProperty(PSCMRWSTATE pState, const char *pszName, const char *pszValue)
1564{
1565 /*
1566 * Update any existing entry first.
1567 */
1568 size_t i = pState->cSvnPropChanges;
1569 while (i-- > 0)
1570 if (!strcmp(pState->paSvnPropChanges[i].pszName, pszName))
1571 {
1572 if (!pszValue)
1573 {
1574 RTStrFree(pState->paSvnPropChanges[i].pszValue);
1575 pState->paSvnPropChanges[i].pszValue = NULL;
1576 }
1577 else
1578 {
1579 char *pszCopy;
1580 int rc = RTStrDupEx(&pszCopy, pszValue);
1581 if (RT_FAILURE(rc))
1582 return rc;
1583 pState->paSvnPropChanges[i].pszValue = pszCopy;
1584 }
1585 return VINF_SUCCESS;
1586 }
1587
1588 /*
1589 * Insert a new entry.
1590 */
1591 i = pState->cSvnPropChanges;
1592 if ((i % 32) == 0)
1593 {
1594 void *pvNew = RTMemRealloc(pState->paSvnPropChanges, (i + 32) * sizeof(SCMSVNPROP));
1595 if (!pvNew)
1596 return VERR_NO_MEMORY;
1597 pState->paSvnPropChanges = (PSCMSVNPROP)pvNew;
1598 }
1599
1600 pState->paSvnPropChanges[i].pszName = RTStrDup(pszName);
1601 pState->paSvnPropChanges[i].pszValue = pszValue ? RTStrDup(pszValue) : NULL;
1602 if ( pState->paSvnPropChanges[i].pszName
1603 && (pState->paSvnPropChanges[i].pszValue || !pszValue) )
1604 pState->cSvnPropChanges = i + 1;
1605 else
1606 {
1607 RTStrFree(pState->paSvnPropChanges[i].pszName);
1608 pState->paSvnPropChanges[i].pszName = NULL;
1609 RTStrFree(pState->paSvnPropChanges[i].pszValue);
1610 pState->paSvnPropChanges[i].pszValue = NULL;
1611 return VERR_NO_MEMORY;
1612 }
1613 return VINF_SUCCESS;
1614}
1615
1616
1617/**
1618 * Schedules a property deletion.
1619 *
1620 * @returns IPRT status code.
1621 * @param pState The rewrite state to work on.
1622 * @param pszName The name of the property to delete.
1623 */
1624int ScmSvnDelProperty(PSCMRWSTATE pState, const char *pszName)
1625{
1626 return ScmSvnSetProperty(pState, pszName, NULL);
1627}
1628
1629
1630/**
1631 * Applies any SVN property changes to the work copy of the file.
1632 *
1633 * @returns IPRT status code.
1634 * @param pState The rewrite state which SVN property changes
1635 * should be applied.
1636 */
1637int ScmSvnDisplayChanges(PSCMRWSTATE pState)
1638{
1639 size_t i = pState->cSvnPropChanges;
1640 while (i-- > 0)
1641 {
1642 const char *pszName = pState->paSvnPropChanges[i].pszName;
1643 const char *pszValue = pState->paSvnPropChanges[i].pszValue;
1644 if (pszValue)
1645 ScmVerbose(pState, 0, "svn propset '%s' '%s' %s\n", pszName, pszValue, pState->pszFilename);
1646 else
1647 ScmVerbose(pState, 0, "svn propdel '%s' %s\n", pszName, pState->pszFilename);
1648 }
1649
1650 return VINF_SUCCESS;
1651}
1652
1653/**
1654 * Applies any SVN property changes to the work copy of the file.
1655 *
1656 * @returns IPRT status code.
1657 * @param pState The rewrite state which SVN property changes
1658 * should be applied.
1659 */
1660int ScmSvnApplyChanges(PSCMRWSTATE pState)
1661{
1662#ifdef SCM_WITH_LATER
1663 if (0)
1664 {
1665 return ...;
1666 }
1667
1668 /* Fallback: */
1669#endif
1670
1671 /*
1672 * Iterate thru the changes and apply them by starting the svn client.
1673 */
1674 for (size_t i = 0; i < pState->cSvnPropChanges; i++)
1675 {
1676 const char *apszArgv[6];
1677 apszArgv[0] = g_szSvnPath;
1678 apszArgv[1] = pState->paSvnPropChanges[i].pszValue ? "propset" : "propdel";
1679 apszArgv[2] = pState->paSvnPropChanges[i].pszName;
1680 int iArg = 3;
1681 if (pState->paSvnPropChanges[i].pszValue)
1682 apszArgv[iArg++] = pState->paSvnPropChanges[i].pszValue;
1683 apszArgv[iArg++] = pState->pszFilename;
1684 apszArgv[iArg++] = NULL;
1685
1686 int rc = scmSvnRun(pState, apszArgv, false);
1687 if (RT_FAILURE(rc))
1688 return rc;
1689 }
1690
1691 return VINF_SUCCESS;
1692}
1693
1694
1695/**
1696 * Initializes the subversion interface.
1697 */
1698void ScmSvnInit(void)
1699{
1700 scmSvnFindSvnBinary(NULL);
1701}
1702
1703
1704void ScmSvnTerm(void)
1705{
1706#ifdef SCM_WITH_DYNAMIC_LIB_SVN
1707 scmSvnFlushClientContextAndPool();
1708#endif
1709}
Note: See TracBrowser for help on using the repository browser.

© 2024 Oracle Support Privacy / Do Not Sell My Info Terms of Use Trademark Policy Automated Access Etiquette