status.c revision 262253
1/*
2 * status.c:  the command-line's portion of the "svn status" command
3 *
4 * ====================================================================
5 *    Licensed to the Apache Software Foundation (ASF) under one
6 *    or more contributor license agreements.  See the NOTICE file
7 *    distributed with this work for additional information
8 *    regarding copyright ownership.  The ASF licenses this file
9 *    to you under the Apache License, Version 2.0 (the
10 *    "License"); you may not use this file except in compliance
11 *    with the License.  You may obtain a copy of the License at
12 *
13 *      http://www.apache.org/licenses/LICENSE-2.0
14 *
15 *    Unless required by applicable law or agreed to in writing,
16 *    software distributed under the License is distributed on an
17 *    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
18 *    KIND, either express or implied.  See the License for the
19 *    specific language governing permissions and limitations
20 *    under the License.
21 * ====================================================================
22 */
23
24/* ==================================================================== */
25
26
27
28/*** Includes. ***/
29#include "svn_hash.h"
30#include "svn_cmdline.h"
31#include "svn_wc.h"
32#include "svn_dirent_uri.h"
33#include "svn_xml.h"
34#include "svn_time.h"
35#include "cl.h"
36#include "svn_private_config.h"
37#include "cl-conflicts.h"
38#include "private/svn_wc_private.h"
39
40/* Return the single character representation of STATUS */
41static char
42generate_status_code(enum svn_wc_status_kind status)
43{
44  switch (status)
45    {
46    case svn_wc_status_none:        return ' ';
47    case svn_wc_status_normal:      return ' ';
48    case svn_wc_status_added:       return 'A';
49    case svn_wc_status_missing:     return '!';
50    case svn_wc_status_incomplete:  return '!';
51    case svn_wc_status_deleted:     return 'D';
52    case svn_wc_status_replaced:    return 'R';
53    case svn_wc_status_modified:    return 'M';
54    case svn_wc_status_conflicted:  return 'C';
55    case svn_wc_status_obstructed:  return '~';
56    case svn_wc_status_ignored:     return 'I';
57    case svn_wc_status_external:    return 'X';
58    case svn_wc_status_unversioned: return '?';
59    default:                        return '?';
60    }
61}
62
63/* Return the combined STATUS as shown in 'svn status' based
64   on the node status and text status */
65static enum svn_wc_status_kind
66combined_status(const svn_client_status_t *status)
67{
68  enum svn_wc_status_kind new_status = status->node_status;
69
70  switch (status->node_status)
71    {
72      case svn_wc_status_conflicted:
73        if (!status->versioned && status->conflicted)
74          {
75            /* Report unversioned tree conflict victims as missing: '!' */
76            new_status = svn_wc_status_missing;
77            break;
78          }
79        /* fall through */
80      case svn_wc_status_modified:
81        /* This value might be the property status */
82        new_status = status->text_status;
83        break;
84      default:
85        break;
86    }
87
88  return new_status;
89}
90
91/* Return the combined repository STATUS as shown in 'svn status' based
92   on the repository node status and repository text status */
93static enum svn_wc_status_kind
94combined_repos_status(const svn_client_status_t *status)
95{
96  if (status->repos_node_status == svn_wc_status_modified)
97    return status->repos_text_status;
98
99  return status->repos_node_status;
100}
101
102/* Return the single character representation of the switched column
103   status. */
104static char
105generate_switch_column_code(const svn_client_status_t *status)
106{
107  if (status->switched)
108    return 'S';
109  else if (status->file_external)
110    return 'X';
111  else
112    return ' ';
113}
114
115/* Return the detailed string representation of STATUS */
116static const char *
117generate_status_desc(enum svn_wc_status_kind status)
118{
119  switch (status)
120    {
121    case svn_wc_status_none:        return "none";
122    case svn_wc_status_normal:      return "normal";
123    case svn_wc_status_added:       return "added";
124    case svn_wc_status_missing:     return "missing";
125    case svn_wc_status_incomplete:  return "incomplete";
126    case svn_wc_status_deleted:     return "deleted";
127    case svn_wc_status_replaced:    return "replaced";
128    case svn_wc_status_modified:    return "modified";
129    case svn_wc_status_conflicted:  return "conflicted";
130    case svn_wc_status_obstructed:  return "obstructed";
131    case svn_wc_status_ignored:     return "ignored";
132    case svn_wc_status_external:    return "external";
133    case svn_wc_status_unversioned: return "unversioned";
134    default:
135      SVN_ERR_MALFUNCTION_NO_RETURN();
136    }
137}
138
139/* Make a relative path containing '..' elements as needed.
140   TARGET_ABSPATH shall be the absolute version of TARGET_PATH.
141   TARGET_ABSPATH, TARGET_PATH and PATH shall be canonical.
142
143   If above conditions are met, a relative path that leads to PATH
144   from TARGET_PATH is returned, but there is no error checking involved.
145
146   The returned path is allocated from RESULT_POOL, all other
147   allocations are made in SCRATCH_POOL.  */
148static const char *
149make_relpath(const char *target_abspath,
150             const char *target_path,
151             const char *path,
152             apr_pool_t *result_pool,
153             apr_pool_t *scratch_pool)
154{
155  const char *la;
156  const char *parent_dir_els = "";
157  const char *abspath, *relative;
158  svn_error_t *err = svn_dirent_get_absolute(&abspath, path, scratch_pool);
159
160  if (err)
161    {
162      /* We probably got passed some invalid path. */
163      svn_error_clear(err);
164      return apr_pstrdup(result_pool, path);
165    }
166
167  relative = svn_dirent_skip_ancestor(target_abspath, abspath);
168  if (relative)
169    {
170      return svn_dirent_join(target_path, relative, result_pool);
171    }
172
173  /* An example:
174   *  relative_to_path = /a/b/c
175   *  path             = /a/x/y/z
176   *  result           = ../../x/y/z
177   *
178   * Another example (Windows specific):
179   *  relative_to_path = F:/wc
180   *  path             = C:/wc
181   *  result           = C:/wc
182   */
183
184  /* Skip the common ancestor of both paths, here '/a'. */
185  la = svn_dirent_get_longest_ancestor(target_abspath, abspath,
186                                       scratch_pool);
187  if (*la == '\0')
188    {
189      /* Nothing in common: E.g. C:/ vs F:/ on Windows */
190      return apr_pstrdup(result_pool, path);
191    }
192  relative = svn_dirent_skip_ancestor(la, target_abspath);
193  path = svn_dirent_skip_ancestor(la, path);
194
195  /* In above example, we'd now have:
196   *  relative_to_path = b/c
197   *  path             = x/y/z */
198
199  /* Count the elements of relative_to_path and prepend as many '..' elements
200   * to path. */
201  while (*relative)
202    {
203      svn_dirent_split(&relative, NULL, relative,
204                       scratch_pool);
205      parent_dir_els = svn_dirent_join(parent_dir_els, "..", scratch_pool);
206    }
207
208  return svn_dirent_join(parent_dir_els, path, result_pool);
209}
210
211
212/* Print STATUS and PATH in a format determined by DETAILED and
213   SHOW_LAST_COMMITTED. */
214static svn_error_t *
215print_status(const char *target_abspath,
216             const char *target_path,
217             const char *path,
218             svn_boolean_t detailed,
219             svn_boolean_t show_last_committed,
220             svn_boolean_t repos_locks,
221             const svn_client_status_t *status,
222             unsigned int *text_conflicts,
223             unsigned int *prop_conflicts,
224             unsigned int *tree_conflicts,
225             svn_client_ctx_t *ctx,
226             apr_pool_t *pool)
227{
228  enum svn_wc_status_kind node_status = status->node_status;
229  enum svn_wc_status_kind prop_status = status->prop_status;
230  char tree_status_code = ' ';
231  const char *tree_desc_line = "";
232  const char *moved_from_line = "";
233  const char *moved_to_line = "";
234
235  path = make_relpath(target_abspath, target_path, path, pool, pool);
236
237  /* For historic reasons svn ignores the property status for added nodes, even
238     if these nodes were copied and have local property changes.
239
240     Note that it doesn't do this on replacements, or children of copies.
241
242     ### Our test suite would catch more errors if we reported property
243         changes on copies. */
244  if (node_status == svn_wc_status_added)
245      prop_status = svn_wc_status_none;
246
247  /* To indicate this node is the victim of a tree conflict, we show
248     'C' in the tree-conflict column, overriding any other status.
249     We also print a separate line describing the nature of the tree
250     conflict. */
251  if (status->conflicted)
252    {
253      const char *desc;
254      const char *local_abspath = status->local_abspath;
255      svn_boolean_t text_conflicted;
256      svn_boolean_t prop_conflicted;
257      svn_boolean_t tree_conflicted;
258
259      if (status->versioned)
260        {
261          svn_error_t *err;
262
263          err = svn_wc_conflicted_p3(&text_conflicted,
264                                     &prop_conflicted,
265                                     &tree_conflicted, ctx->wc_ctx,
266                                     local_abspath, pool);
267
268          if (err && err->apr_err == SVN_ERR_WC_UPGRADE_REQUIRED)
269            {
270              svn_error_clear(err);
271              text_conflicted = FALSE;
272              prop_conflicted = FALSE;
273              tree_conflicted = FALSE;
274            }
275          else
276            SVN_ERR(err);
277        }
278      else
279        {
280          text_conflicted = FALSE;
281          prop_conflicted = FALSE;
282          tree_conflicted = TRUE;
283        }
284
285      if (tree_conflicted)
286        {
287          const svn_wc_conflict_description2_t *tree_conflict;
288          SVN_ERR(svn_wc__get_tree_conflict(&tree_conflict, ctx->wc_ctx,
289                                            local_abspath, pool, pool));
290          SVN_ERR_ASSERT(tree_conflict != NULL);
291
292          tree_status_code = 'C';
293          SVN_ERR(svn_cl__get_human_readable_tree_conflict_description(
294                            &desc, tree_conflict, pool));
295          tree_desc_line = apr_psprintf(pool, "\n      >   %s", desc);
296          (*tree_conflicts)++;
297        }
298      else if (text_conflicted)
299        (*text_conflicts)++;
300      else if (prop_conflicted)
301        (*prop_conflicts)++;
302    }
303
304  /* Note that moved-from and moved-to information is only available in STATUS
305   * for (op-)roots of a move. Those are exactly the nodes we want to show
306   * move info for in 'svn status'. See also comments in svn_wc_status3_t. */
307  if (status->moved_from_abspath && status->moved_to_abspath &&
308      strcmp(status->moved_from_abspath, status->moved_to_abspath) == 0)
309    {
310      const char *relpath;
311
312      relpath = make_relpath(target_abspath, target_path,
313                             status->moved_from_abspath,
314                             pool, pool);
315      relpath = svn_dirent_local_style(relpath, pool);
316      moved_from_line = apr_pstrcat(pool, "\n        > ",
317                                    apr_psprintf(pool,
318                                                 _("swapped places with %s"),
319                                                 relpath),
320                                    (char *)NULL);
321    }
322  else if (status->moved_from_abspath || status->moved_to_abspath)
323    {
324      const char *relpath;
325
326      if (status->moved_from_abspath)
327        {
328          relpath = make_relpath(target_abspath, target_path,
329                                 status->moved_from_abspath,
330                                 pool, pool);
331          relpath = svn_dirent_local_style(relpath, pool);
332          moved_from_line = apr_pstrcat(pool, "\n        > ",
333                                        apr_psprintf(pool, _("moved from %s"),
334                                                     relpath),
335                                        (char *)NULL);
336        }
337
338      if (status->moved_to_abspath)
339        {
340          relpath = make_relpath(target_abspath, target_path,
341                                 status->moved_to_abspath,
342                                 pool, pool);
343          relpath = svn_dirent_local_style(relpath, pool);
344          moved_to_line = apr_pstrcat(pool, "\n        > ",
345                                      apr_psprintf(pool, _("moved to %s"),
346                                                   relpath),
347                                      (char *)NULL);
348        }
349    }
350
351  path = svn_dirent_local_style(path, pool);
352
353  if (detailed)
354    {
355      char ood_status, lock_status;
356      const char *working_rev;
357
358      if (! status->versioned)
359        working_rev = "";
360      else if (status->copied
361               || ! SVN_IS_VALID_REVNUM(status->revision))
362        working_rev = "-";
363      else
364        working_rev = apr_psprintf(pool, "%ld", status->revision);
365
366      if (status->repos_node_status != svn_wc_status_none)
367        ood_status = '*';
368      else
369        ood_status = ' ';
370
371      if (repos_locks)
372        {
373          if (status->repos_lock)
374            {
375              if (status->lock)
376                {
377                  if (strcmp(status->repos_lock->token, status->lock->token)
378                      == 0)
379                    lock_status = 'K';
380                  else
381                    lock_status = 'T';
382                }
383              else
384                lock_status = 'O';
385            }
386          else if (status->lock)
387            lock_status = 'B';
388          else
389            lock_status = ' ';
390        }
391      else
392        lock_status = (status->lock) ? 'K' : ' ';
393
394      if (show_last_committed)
395        {
396          const char *commit_rev;
397          const char *commit_author;
398
399          if (SVN_IS_VALID_REVNUM(status->changed_rev))
400            commit_rev = apr_psprintf(pool, "%ld", status->changed_rev);
401          else if (status->versioned)
402            commit_rev = " ? ";
403          else
404            commit_rev = "";
405
406          if (status->changed_author)
407            commit_author = status->changed_author;
408          else if (status->versioned)
409            commit_author = " ? ";
410          else
411            commit_author = "";
412
413          SVN_ERR
414            (svn_cmdline_printf(pool,
415                                "%c%c%c%c%c%c%c %c %8s %8s %-12s %s%s%s%s\n",
416                                generate_status_code(combined_status(status)),
417                                generate_status_code(prop_status),
418                                status->wc_is_locked ? 'L' : ' ',
419                                status->copied ? '+' : ' ',
420                                generate_switch_column_code(status),
421                                lock_status,
422                                tree_status_code,
423                                ood_status,
424                                working_rev,
425                                commit_rev,
426                                commit_author,
427                                path,
428                                moved_to_line,
429                                moved_from_line,
430                                tree_desc_line));
431        }
432      else
433        SVN_ERR(
434           svn_cmdline_printf(pool, "%c%c%c%c%c%c%c %c %8s   %s%s%s%s\n",
435                              generate_status_code(combined_status(status)),
436                              generate_status_code(prop_status),
437                              status->wc_is_locked ? 'L' : ' ',
438                              status->copied ? '+' : ' ',
439                              generate_switch_column_code(status),
440                              lock_status,
441                              tree_status_code,
442                              ood_status,
443                              working_rev,
444                              path,
445                              moved_to_line,
446                              moved_from_line,
447                              tree_desc_line));
448    }
449  else
450    SVN_ERR(
451       svn_cmdline_printf(pool, "%c%c%c%c%c%c%c %s%s%s%s\n",
452                          generate_status_code(combined_status(status)),
453                          generate_status_code(prop_status),
454                          status->wc_is_locked ? 'L' : ' ',
455                          status->copied ? '+' : ' ',
456                          generate_switch_column_code(status),
457                          ((status->lock)
458                           ? 'K' : ' '),
459                          tree_status_code,
460                          path,
461                          moved_to_line,
462                          moved_from_line,
463                          tree_desc_line));
464
465  return svn_cmdline_fflush(stdout);
466}
467
468
469svn_error_t *
470svn_cl__print_status_xml(const char *target_abspath,
471                         const char *target_path,
472                         const char *path,
473                         const svn_client_status_t *status,
474                         svn_client_ctx_t *ctx,
475                         apr_pool_t *pool)
476{
477  svn_stringbuf_t *sb = svn_stringbuf_create_empty(pool);
478  apr_hash_t *att_hash;
479  const char *local_abspath = status->local_abspath;
480  svn_boolean_t tree_conflicted = FALSE;
481
482  if (status->node_status == svn_wc_status_none
483      && status->repos_node_status == svn_wc_status_none)
484    return SVN_NO_ERROR;
485
486  if (status->conflicted)
487    SVN_ERR(svn_wc_conflicted_p3(NULL, NULL, &tree_conflicted,
488                                 ctx->wc_ctx, local_abspath, pool));
489
490  path = make_relpath(target_abspath, target_path, path, pool, pool);
491
492  svn_xml_make_open_tag(&sb, pool, svn_xml_normal, "entry",
493                        "path", svn_dirent_local_style(path, pool), NULL);
494
495  att_hash = apr_hash_make(pool);
496  svn_hash_sets(att_hash, "item",
497                generate_status_desc(combined_status(status)));
498
499  svn_hash_sets(att_hash, "props",
500                generate_status_desc(
501                   (status->node_status != svn_wc_status_deleted)
502                   ? status->prop_status
503                   : svn_wc_status_none));
504  if (status->wc_is_locked)
505    svn_hash_sets(att_hash, "wc-locked", "true");
506  if (status->copied)
507    svn_hash_sets(att_hash, "copied", "true");
508  if (status->switched)
509    svn_hash_sets(att_hash, "switched", "true");
510  if (status->file_external)
511    svn_hash_sets(att_hash, "file-external", "true");
512  if (status->versioned && ! status->copied)
513    svn_hash_sets(att_hash, "revision",
514                  apr_psprintf(pool, "%ld", status->revision));
515  if (tree_conflicted)
516    svn_hash_sets(att_hash, "tree-conflicted", "true");
517  if (status->moved_from_abspath || status->moved_to_abspath)
518    {
519      const char *relpath;
520
521      if (status->moved_from_abspath)
522        {
523          relpath = make_relpath(target_abspath, target_path,
524                                 status->moved_from_abspath,
525                                 pool, pool);
526          relpath = svn_dirent_local_style(relpath, pool);
527          svn_hash_sets(att_hash, "moved-from", relpath);
528        }
529      if (status->moved_to_abspath)
530        {
531          relpath = make_relpath(target_abspath, target_path,
532                                 status->moved_to_abspath,
533                                 pool, pool);
534          relpath = svn_dirent_local_style(relpath, pool);
535          svn_hash_sets(att_hash, "moved-to", relpath);
536        }
537    }
538  svn_xml_make_open_tag_hash(&sb, pool, svn_xml_normal, "wc-status",
539                             att_hash);
540
541  if (SVN_IS_VALID_REVNUM(status->changed_rev))
542    {
543      svn_cl__print_xml_commit(&sb, status->changed_rev,
544                               status->changed_author,
545                               svn_time_to_cstring(status->changed_date,
546                                                   pool),
547                               pool);
548    }
549
550  if (status->lock)
551    svn_cl__print_xml_lock(&sb, status->lock, pool);
552
553  svn_xml_make_close_tag(&sb, pool, "wc-status");
554
555  if (status->repos_node_status != svn_wc_status_none
556      || status->repos_lock)
557    {
558      svn_xml_make_open_tag(&sb, pool, svn_xml_normal, "repos-status",
559                            "item",
560                            generate_status_desc(combined_repos_status(status)),
561                            "props",
562                            generate_status_desc(status->repos_prop_status),
563                            NULL);
564      if (status->repos_lock)
565        svn_cl__print_xml_lock(&sb, status->repos_lock, pool);
566
567      svn_xml_make_close_tag(&sb, pool, "repos-status");
568    }
569
570  svn_xml_make_close_tag(&sb, pool, "entry");
571
572  return svn_cl__error_checked_fputs(sb->data, stdout);
573}
574
575/* Called by status-cmd.c */
576svn_error_t *
577svn_cl__print_status(const char *target_abspath,
578                     const char *target_path,
579                     const char *path,
580                     const svn_client_status_t *status,
581                     svn_boolean_t suppress_externals_placeholders,
582                     svn_boolean_t detailed,
583                     svn_boolean_t show_last_committed,
584                     svn_boolean_t skip_unrecognized,
585                     svn_boolean_t repos_locks,
586                     unsigned int *text_conflicts,
587                     unsigned int *prop_conflicts,
588                     unsigned int *tree_conflicts,
589                     svn_client_ctx_t *ctx,
590                     apr_pool_t *pool)
591{
592  if (! status
593      || (skip_unrecognized
594          && !(status->versioned
595               || status->conflicted
596               || status->node_status == svn_wc_status_external))
597      || (status->node_status == svn_wc_status_none
598          && status->repos_node_status == svn_wc_status_none))
599    return SVN_NO_ERROR;
600
601  /* If we're trying not to print boring "X  /path/to/external"
602     lines..." */
603  if (suppress_externals_placeholders)
604    {
605      /* ... skip regular externals unmodified in the repository. */
606      if ((status->node_status == svn_wc_status_external)
607          && (status->repos_node_status == svn_wc_status_none)
608          && (! status->conflicted))
609        return SVN_NO_ERROR;
610
611      /* ... skip file externals that aren't modified locally or
612         remotely, changelisted, or locked (in either sense of the
613         word). */
614      if ((status->file_external)
615          && (status->repos_node_status == svn_wc_status_none)
616          && ((status->node_status == svn_wc_status_normal)
617              || (status->node_status == svn_wc_status_none))
618          && ((status->prop_status == svn_wc_status_normal)
619              || (status->prop_status == svn_wc_status_none))
620          && (! status->changelist)
621          && (! status->lock)
622          && (! status->wc_is_locked)
623          && (! status->conflicted))
624        return SVN_NO_ERROR;
625    }
626
627  return print_status(target_abspath, target_path, path,
628                      detailed, show_last_committed, repos_locks, status,
629                      text_conflicts, prop_conflicts, tree_conflicts,
630                      ctx, pool);
631}
632