status.c revision 299742
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 LOCAL_ABSPATH 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.
148
149   Note that it is not possible to just join the resulting path to
150   reconstruct the real path as the "../" paths are relative from
151   a different base than the normal relative paths!
152 */
153static const char *
154make_relpath(const char *target_abspath,
155             const char *target_path,
156             const char *local_abspath,
157             apr_pool_t *result_pool,
158             apr_pool_t *scratch_pool)
159{
160  const char *la;
161  const char *parent_dir_els = "";
162  const char *t_relpath;
163  const char *p_relpath;
164
165#ifdef SVN_DEBUG
166  SVN_ERR_ASSERT_NO_RETURN(svn_dirent_is_absolute(local_abspath));
167#endif
168
169  t_relpath = svn_dirent_skip_ancestor(target_abspath, local_abspath);
170
171  if (t_relpath)
172    return svn_dirent_join(target_path, t_relpath, result_pool);
173
174  /* An example:
175   *  relative_to_path = /a/b/c
176   *  path             = /a/x/y/z
177   *  result           = ../../x/y/z
178   *
179   * Another example (Windows specific):
180   *  relative_to_path = F:/wc
181   *  path             = C:/wc
182   *  result           = C:/wc
183   */
184  /* Skip the common ancestor of both paths, here '/a'. */
185  la = svn_dirent_get_longest_ancestor(target_abspath, local_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, local_abspath);
191    }
192  t_relpath = svn_dirent_skip_ancestor(la, target_abspath);
193  p_relpath = svn_dirent_skip_ancestor(la, local_abspath);
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 (*t_relpath)
202    {
203      t_relpath = svn_dirent_dirname(t_relpath, scratch_pool);
204      parent_dir_els = svn_dirent_join(parent_dir_els, "..", scratch_pool);
205    }
206
207  /* This returns a ../ style path relative from the status target */
208  return svn_dirent_join(parent_dir_els, p_relpath, 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  /* For historic reasons svn ignores the property status for added nodes, even
236     if these nodes were copied and have local property changes.
237
238     Note that it doesn't do this on replacements, or children of copies.
239
240     ### Our test suite would catch more errors if we reported property
241         changes on copies. */
242  if (node_status == svn_wc_status_added)
243      prop_status = svn_wc_status_none;
244
245  /* To indicate this node is the victim of a tree conflict, we show
246     'C' in the tree-conflict column, overriding any other status.
247     We also print a separate line describing the nature of the tree
248     conflict. */
249  if (status->conflicted)
250    {
251      const char *desc;
252      const char *local_abspath = status->local_abspath;
253      svn_boolean_t text_conflicted;
254      svn_boolean_t prop_conflicted;
255      svn_boolean_t tree_conflicted;
256
257      if (status->versioned)
258        {
259          svn_error_t *err;
260
261          err = svn_wc_conflicted_p3(&text_conflicted,
262                                     &prop_conflicted,
263                                     &tree_conflicted, ctx->wc_ctx,
264                                     local_abspath, pool);
265
266          if (err && err->apr_err == SVN_ERR_WC_UPGRADE_REQUIRED)
267            {
268              svn_error_clear(err);
269              text_conflicted = FALSE;
270              prop_conflicted = FALSE;
271              tree_conflicted = FALSE;
272            }
273          else
274            SVN_ERR(err);
275        }
276      else
277        {
278          text_conflicted = FALSE;
279          prop_conflicted = FALSE;
280          tree_conflicted = TRUE;
281        }
282
283      if (tree_conflicted)
284        {
285          const svn_wc_conflict_description2_t *tree_conflict;
286          SVN_ERR(svn_wc__get_tree_conflict(&tree_conflict, ctx->wc_ctx,
287                                            local_abspath, pool, pool));
288          SVN_ERR_ASSERT(tree_conflict != NULL);
289
290          tree_status_code = 'C';
291          SVN_ERR(svn_cl__get_human_readable_tree_conflict_description(
292                            &desc, tree_conflict, pool));
293          tree_desc_line = apr_psprintf(pool, "\n      >   %s", desc);
294          (*tree_conflicts)++;
295        }
296      else if (text_conflicted)
297        (*text_conflicts)++;
298      else if (prop_conflicted)
299        (*prop_conflicts)++;
300    }
301
302  /* Note that moved-from and moved-to information is only available in STATUS
303   * for (op-)roots of a move. Those are exactly the nodes we want to show
304   * move info for in 'svn status'. See also comments in svn_wc_status3_t. */
305  if (status->moved_from_abspath && status->moved_to_abspath &&
306      strcmp(status->moved_from_abspath, status->moved_to_abspath) == 0)
307    {
308      const char *relpath;
309
310      relpath = make_relpath(target_abspath, target_path,
311                             status->moved_from_abspath,
312                             pool, pool);
313      relpath = svn_dirent_local_style(relpath, pool);
314      moved_from_line = apr_pstrcat(pool, "\n        > ",
315                                    apr_psprintf(pool,
316                                                 _("swapped places with %s"),
317                                                 relpath),
318                                    SVN_VA_NULL);
319    }
320  else if (status->moved_from_abspath || status->moved_to_abspath)
321    {
322      const char *relpath;
323
324      if (status->moved_from_abspath)
325        {
326          relpath = make_relpath(target_abspath, target_path,
327                                 status->moved_from_abspath,
328                                 pool, pool);
329          relpath = svn_dirent_local_style(relpath, pool);
330          moved_from_line = apr_pstrcat(pool, "\n        > ",
331                                        apr_psprintf(pool, _("moved from %s"),
332                                                     relpath),
333                                        SVN_VA_NULL);
334        }
335
336      if (status->moved_to_abspath)
337        {
338          relpath = make_relpath(target_abspath, target_path,
339                                 status->moved_to_abspath,
340                                 pool, pool);
341          relpath = svn_dirent_local_style(relpath, pool);
342          moved_to_line = apr_pstrcat(pool, "\n        > ",
343                                      apr_psprintf(pool, _("moved to %s"),
344                                                   relpath),
345                                      SVN_VA_NULL);
346        }
347    }
348
349  path = svn_dirent_local_style(path, pool);
350
351  if (detailed)
352    {
353      char ood_status, lock_status;
354      const char *working_rev;
355
356      if (! status->versioned)
357        working_rev = "";
358      else if (status->copied
359               || ! SVN_IS_VALID_REVNUM(status->revision))
360        working_rev = "-";
361      else
362        working_rev = apr_psprintf(pool, "%ld", status->revision);
363
364      if (status->repos_node_status != svn_wc_status_none)
365        ood_status = '*';
366      else
367        ood_status = ' ';
368
369      if (repos_locks)
370        {
371          if (status->repos_lock)
372            {
373              if (status->lock)
374                {
375                  if (strcmp(status->repos_lock->token, status->lock->token)
376                      == 0)
377                    lock_status = 'K';
378                  else
379                    lock_status = 'T';
380                }
381              else
382                lock_status = 'O';
383            }
384          else if (status->lock)
385            lock_status = 'B';
386          else
387            lock_status = ' ';
388        }
389      else
390        lock_status = (status->lock) ? 'K' : ' ';
391
392      if (show_last_committed)
393        {
394          const char *commit_rev;
395          const char *commit_author;
396
397          if (SVN_IS_VALID_REVNUM(status->changed_rev))
398            commit_rev = apr_psprintf(pool, "%ld", status->changed_rev);
399          else if (status->versioned)
400            commit_rev = " ? ";
401          else
402            commit_rev = "";
403
404          if (status->changed_author)
405            commit_author = status->changed_author;
406          else if (status->versioned)
407            commit_author = " ? ";
408          else
409            commit_author = "";
410
411          SVN_ERR
412            (svn_cmdline_printf(pool,
413                                "%c%c%c%c%c%c%c %c %8s %8s %-12s %s%s%s%s\n",
414                                generate_status_code(combined_status(status)),
415                                generate_status_code(prop_status),
416                                status->wc_is_locked ? 'L' : ' ',
417                                status->copied ? '+' : ' ',
418                                generate_switch_column_code(status),
419                                lock_status,
420                                tree_status_code,
421                                ood_status,
422                                working_rev,
423                                commit_rev,
424                                commit_author,
425                                path,
426                                moved_to_line,
427                                moved_from_line,
428                                tree_desc_line));
429        }
430      else
431        SVN_ERR(
432           svn_cmdline_printf(pool, "%c%c%c%c%c%c%c %c %8s   %s%s%s%s\n",
433                              generate_status_code(combined_status(status)),
434                              generate_status_code(prop_status),
435                              status->wc_is_locked ? 'L' : ' ',
436                              status->copied ? '+' : ' ',
437                              generate_switch_column_code(status),
438                              lock_status,
439                              tree_status_code,
440                              ood_status,
441                              working_rev,
442                              path,
443                              moved_to_line,
444                              moved_from_line,
445                              tree_desc_line));
446    }
447  else
448    SVN_ERR(
449       svn_cmdline_printf(pool, "%c%c%c%c%c%c%c %s%s%s%s\n",
450                          generate_status_code(combined_status(status)),
451                          generate_status_code(prop_status),
452                          status->wc_is_locked ? 'L' : ' ',
453                          status->copied ? '+' : ' ',
454                          generate_switch_column_code(status),
455                          ((status->lock)
456                           ? 'K' : ' '),
457                          tree_status_code,
458                          path,
459                          moved_to_line,
460                          moved_from_line,
461                          tree_desc_line));
462
463  return svn_cmdline_fflush(stdout);
464}
465
466
467svn_error_t *
468svn_cl__print_status_xml(const char *target_abspath,
469                         const char *target_path,
470                         const char *path,
471                         const svn_client_status_t *status,
472                         svn_client_ctx_t *ctx,
473                         apr_pool_t *pool)
474{
475  svn_stringbuf_t *sb = svn_stringbuf_create_empty(pool);
476  apr_hash_t *att_hash;
477  const char *local_abspath = status->local_abspath;
478  svn_boolean_t tree_conflicted = FALSE;
479
480  if (status->node_status == svn_wc_status_none
481      && status->repos_node_status == svn_wc_status_none)
482    return SVN_NO_ERROR;
483
484  if (status->conflicted)
485    SVN_ERR(svn_wc_conflicted_p3(NULL, NULL, &tree_conflicted,
486                                 ctx->wc_ctx, local_abspath, pool));
487
488  svn_xml_make_open_tag(&sb, pool, svn_xml_normal, "entry",
489                        "path", svn_dirent_local_style(path, pool),
490                        SVN_VA_NULL);
491
492  att_hash = apr_hash_make(pool);
493  svn_hash_sets(att_hash, "item",
494                generate_status_desc(combined_status(status)));
495
496  svn_hash_sets(att_hash, "props",
497                generate_status_desc(
498                   (status->node_status != svn_wc_status_deleted)
499                   ? status->prop_status
500                   : svn_wc_status_none));
501  if (status->wc_is_locked)
502    svn_hash_sets(att_hash, "wc-locked", "true");
503  if (status->copied)
504    svn_hash_sets(att_hash, "copied", "true");
505  if (status->switched)
506    svn_hash_sets(att_hash, "switched", "true");
507  if (status->file_external)
508    svn_hash_sets(att_hash, "file-external", "true");
509  if (status->versioned && ! status->copied)
510    svn_hash_sets(att_hash, "revision",
511                  apr_psprintf(pool, "%ld", status->revision));
512  if (tree_conflicted)
513    svn_hash_sets(att_hash, "tree-conflicted", "true");
514  if (status->moved_from_abspath || status->moved_to_abspath)
515    {
516      const char *relpath;
517
518      if (status->moved_from_abspath)
519        {
520          relpath = make_relpath(target_abspath, target_path,
521                                 status->moved_from_abspath,
522                                 pool, pool);
523          relpath = svn_dirent_local_style(relpath, pool);
524          svn_hash_sets(att_hash, "moved-from", relpath);
525        }
526      if (status->moved_to_abspath)
527        {
528          relpath = make_relpath(target_abspath, target_path,
529                                 status->moved_to_abspath,
530                                 pool, pool);
531          relpath = svn_dirent_local_style(relpath, pool);
532          svn_hash_sets(att_hash, "moved-to", relpath);
533        }
534    }
535  svn_xml_make_open_tag_hash(&sb, pool, svn_xml_normal, "wc-status",
536                             att_hash);
537
538  if (SVN_IS_VALID_REVNUM(status->changed_rev))
539    {
540      svn_cl__print_xml_commit(&sb, status->changed_rev,
541                               status->changed_author,
542                               svn_time_to_cstring(status->changed_date,
543                                                   pool),
544                               pool);
545    }
546
547  if (status->lock)
548    svn_cl__print_xml_lock(&sb, status->lock, pool);
549
550  svn_xml_make_close_tag(&sb, pool, "wc-status");
551
552  if (status->repos_node_status != svn_wc_status_none
553      || status->repos_lock)
554    {
555      svn_xml_make_open_tag(&sb, pool, svn_xml_normal, "repos-status",
556                            "item",
557                            generate_status_desc(combined_repos_status(status)),
558                            "props",
559                            generate_status_desc(status->repos_prop_status),
560                            SVN_VA_NULL);
561      if (status->repos_lock)
562        svn_cl__print_xml_lock(&sb, status->repos_lock, pool);
563
564      svn_xml_make_close_tag(&sb, pool, "repos-status");
565    }
566
567  svn_xml_make_close_tag(&sb, pool, "entry");
568
569  return svn_cl__error_checked_fputs(sb->data, stdout);
570}
571
572/* Called by status-cmd.c */
573svn_error_t *
574svn_cl__print_status(const char *target_abspath,
575                     const char *target_path,
576                     const char *path,
577                     const svn_client_status_t *status,
578                     svn_boolean_t suppress_externals_placeholders,
579                     svn_boolean_t detailed,
580                     svn_boolean_t show_last_committed,
581                     svn_boolean_t skip_unrecognized,
582                     svn_boolean_t repos_locks,
583                     unsigned int *text_conflicts,
584                     unsigned int *prop_conflicts,
585                     unsigned int *tree_conflicts,
586                     svn_client_ctx_t *ctx,
587                     apr_pool_t *pool)
588{
589  if (! status
590      || (skip_unrecognized
591          && !(status->versioned
592               || status->conflicted
593               || status->node_status == svn_wc_status_external))
594      || (status->node_status == svn_wc_status_none
595          && status->repos_node_status == svn_wc_status_none))
596    return SVN_NO_ERROR;
597
598  /* If we're trying not to print boring "X  /path/to/external"
599     lines..." */
600  if (suppress_externals_placeholders)
601    {
602      /* ... skip regular externals unmodified in the repository. */
603      if ((status->node_status == svn_wc_status_external)
604          && (status->repos_node_status == svn_wc_status_none)
605          && (! status->conflicted))
606        return SVN_NO_ERROR;
607
608      /* ... skip file externals that aren't modified locally or
609         remotely, changelisted, or locked (in either sense of the
610         word). */
611      if ((status->file_external)
612          && (status->repos_node_status == svn_wc_status_none)
613          && ((status->node_status == svn_wc_status_normal)
614              || (status->node_status == svn_wc_status_none))
615          && ((status->prop_status == svn_wc_status_normal)
616              || (status->prop_status == svn_wc_status_none))
617          && (! status->changelist)
618          && (! status->lock)
619          && (! status->wc_is_locked)
620          && (! status->conflicted))
621        return SVN_NO_ERROR;
622    }
623
624  return print_status(target_abspath, target_path, path,
625                      detailed, show_last_committed, repos_locks, status,
626                      text_conflicts, prop_conflicts, tree_conflicts,
627                      ctx, pool);
628}
629